1 line
5.2 MiB
Plaintext
1 line
5.2 MiB
Plaintext
{"repo": "cankasaplar/castle-genesis-rhizoh-habitatos", "files": {"apps/gateway/src/server.js": {"3e4bc6cc37644a351d1b9758e25ccbfe26da0483": {"t": 1785745548.649965, "n": 179840, "h": "c0fcfe4e326b826b", "c": "import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport dotenv from \"dotenv\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconsole.log(\"🔥 [GENESIS_BOOT] entry file:\", __filename);\nconsole.log(\"🔥 [GENESIS_BOOT] node env:\", process.env.NODE_ENV);\nconsole.log(\"🔥 [GENESIS_BOOT] pid:\", process.pid);\nconsole.log(\"🧬 [GENESIS_BOOT] cwd:\", process.cwd());\nconsole.log(\"🧬 [GENESIS_BOOT] import.meta.url:\", import.meta.url);\ndotenv.config({ path: path.join(__dirname, \"..\", \".env\") });\ndotenv.config({ path: path.join(__dirname, \"..\", \".env.local\"), override: true });\n\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport { createServer } from \"node:http\";\nimport { createOrchestrator } from \"@castle/orchestrator\";\nimport { parseTextToCommand } from \"@castle/command-dsl\";\nimport {\n WS_MESSAGE,\n COMMAND,\n createEnvelope,\n createRhizohImmutableEventV0,\n createRhizohPayloadRefV0,\n safeJsonParse,\n validateRhizohVoiceLiveChunkPayloadV0\n} from \"@castle/protocol\";\nimport { queryOpenData } from \"./openData.js\";\nimport { verifyClientToken } from \"./auth.js\";\nimport { handleMatchMoveAuthorityV0, leaveMatchBroadcastRoomV0, sendMatchSessionSnapshotOnJoinV0 } from \"./rhizoh/matchMoveAuthorityV0.js\";\nimport { handleMatchSessionJoinV0, handleMatchCastleInviteV0 } from \"./rhizoh/matchBroadcastRoomV0.js\";\nimport { handleMatchStateAppliedV0 } from \"./rhizoh/matchAckAggregatorV0.js\";\nimport {\n listUnifiedGatewayPresenceV0,\n registerGatewayServiceNodeV0,\n unregisterGatewayServiceNodesByClientV0\n} from \"./rhizoh/gatewayPresenceRegistryV0.js\";\nimport {\n enrichVoiceLivePayloadV0,\n handleVoiceStateAppliedV0,\n registerVoiceGatewayCitizenV0,\n resolveVoiceWorldContextV0,\n sendVoiceTranscriptCommittedV0,\n} from \"./rhizoh/voiceGatewayCitizenshipV0.js\";\nimport { queryRhizohLlm } from \"./rhizohLlmGateway.js\";\nimport { rhizohGatewayTurn } from \"./rhizohGatewayTurn.js\";\nimport {\n enqueueLlmWorkerTaskAsyncV0,\n getLatestLlmWorkerTaskIdV0,\n getLlmWorkerDebugSnapshotV0,\n getLlmWorkerTaskSnapshotV0,\n initLlmWorkerAtBootV0,\n isLlmWorkerAsyncEnabledV0,\n isLlmWorkerEnabledV0,\n runLlmWorkerTaskV0\n} from \"./llmWorkerRuntime.js\";\nimport {\n RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n runRhizohVoiceTranscribeV3,\n rhizohVoiceTranscribeEnvV3\n} from \"./rhizohVoiceTranscribeV3.js\";\nimport { createHttpCorsPolicy, normalizeHttpOrigin, parseAllowedOriginsFromEnv } from \"./httpCorsPolicyV1.js\";\nimport {\n applyLanguagePropagationToLlmPayloadV1,\n applyLanguagePropagationToVoiceBodyV1,\n buildRhizohLanguagePropagationEchoV1,\n parseRhizohLanguagePropagationV1\n} from \"./rhizohLanguagePropagationV1.js\";\nimport { trimRhizohLlmGatewayPayloadV1 } from \"./rhizohLlmPayloadTrimV1.js\";\nimport {\n meshAppendDelta,\n meshContinuityAggregate,\n meshJoin,\n meshLeave,\n meshReplayFromNodeId,\n meshReplayFromSeq,\n meshSnapshot,\n meshSubscribeSse\n} from \"./presenceRoomMesh.js\";\nimport { runRhizohBrainV2 } from \"./rhizohBrainV2.js\";\nimport {\n listConnections,\n createConnection,\n updateConnection,\n deleteConnection,\n setDefaultConnection,\n resolveConnection\n} from \"./llmConnectionsStore.js\";\nimport { getCloudSyncSnapshotV0, mergeCloudSyncSnapshotV0 } from \"./castleCloudSyncStore.js\";\nimport {\n CASTLE_NETWORK_SIGNAL_V0,\n handleCastleNetworkSignalV0,\n listCastleNetworkPresenceV0,\n removeCastleNetworkClientV0,\n validateCastleNetworkSignalPayloadV0\n} from \"./castleNetworkRelayV0.js\";\nimport { checkHttpRateLimit, getHttpClientIp } from \"./castleHttpRateLimit.js\";\nimport {\n submitAbuseReportV0,\n listModerationQueueV0,\n softBlockUserV0,\n removeSoftBlockV0,\n listSoftBlockedUsersV0\n} from \"./ops/moderationMvpV0.js\";\nimport { listRecentAgentSnapshotsV0 } from \"./ops/agentObservabilityV0.js\";\nimport { readAgentContainmentConfigV0 } from \"./ops/agentContainmentV0.js\";\nimport { readCostContainmentConfigV0, getCostContainmentStatsV0 } from \"./ops/costContainmentV0.js\";\nimport { getGlobalCostLedgerSnapshotV0, getGclBackendStatusV0 } from \"./ops/globalCostLedgerV0.js\";\nimport { evaluateEconomicStrategyL3V0 } from \"./ops/economicStrategyEngineL3V0.js\";\nimport { getPhasedRolloutStatsV0, readPhasedRolloutTierV0 } from \"./ops/phasedRolloutV0.js\";\nimport { buildUnifiedStateNarrativeV0 } from \"./ops/unifiedStateNarrativeV0.js\";\nimport { resolveHardeningTenantIdV0 } from \"./ops/narrativeTenantIsolationV0.js\";\nimport {\n classifyStressResponseV0,\n verifyStressTaxonomyCoverageV0,\n verifyStressConflictResolutionV0\n} from \"./ops/stressResponseTaxonomyV0.js\";\nimport { verifyResolutionStabilityEnvelopeV0 } from \"./ops/resolutionStabilityEnvelopeV0.js\";\nimport { logLlmAccess } from \"./castleLlmAudit.js\";\nimport { appendMemory, listMemories, getMemoryContext, getPersonaGoalMemory, setPersonaGoalMemory, autoCompactMemories } from \"./memoryStore.js\";\nimport { getFirebasePersistence } from \"./firebasePersistence.js\";\nimport { registerAgentIdentity, listAgentIdentities, getAgentIdentity, updateAgentIdentity } from \"./agentIdentityStore.js\";\nimport { queueAcademyEvent, listAcademyEvents, resolveAcademyEvent, runAcademyEventTick } from \"./academyEventEngine.js\";\nimport { enqueuePdfJob, getPdfJob } from \"./pdfPipeline.js\";\nimport {\n createPublishSession,\n listPublishSessions,\n updatePublishSession,\n addTranscript,\n listTranscripts,\n searchMetadata,\n registerDevice,\n listDevices,\n updateDevice,\n appendTelemetry,\n listTelemetry,\n createAutomation,\n listAutomations,\n tickAutomations,\n enqueueCommand,\n listCommands,\n updateCommandStatus,\n registerSocialChannel,\n listSocialChannels\n} from \"./studioOpsStore.js\";\nimport { metrics as infraMetrics } from \"./infra/metrics.js\";\nimport { renderRhizohPrometheusMetrics, rhizohEnterpriseMetrics } from \"./infra/rhizohEnterpriseMetrics.js\";\nimport { scoreHealth } from \"./infra/healthScore.js\";\nimport { getTraceById, listTracesBySession } from \"./infra/traceRegistry.js\";\nimport { canonicalEpistemicSealString, hashAndSignEpistemicSeal, EPISTEMIC_SEAL_SCHEMA } from \"./epistemicSeal.js\";\nimport { persistEpistemicLedgerBatch } from \"./epistemicLedgerStore.js\";\nimport { persistEpistemicForecastBatch } from \"./epistemicForecastStore.js\";\nimport {\n persistAuthorityLedgerWitnessBatchV1,\n getAuthorityLedgerWitnessSnapshotV1,\n replayAuthorityLedgerWitnessChainV1\n} from \"./authorityLedgerWitnessStoreV1.js\";\nimport { assimilateAuthorityEpochMergeV1 } from \"./authorityEpochMergeStoreV1.js\";\nimport { buildRhizohExternalGroundTruthPayload } from \"./rhizohExternalGroundTruthGateway.js\";\nimport { ingestRhizohExternalLossBatchHttp } from \"./rhizohExternalLossIngestGateway.js\";\nimport {\n ingestRhizohProductOutcomeHttp,\n listRhizohProductOutcomeAggregates,\n verifyRhizohOutcomeHmac,\n verifyRhizohOutcomeSourceToken\n} from \"./rhizohProductOutcomeIngestGateway.js\";\nimport { initRhizoh } from \"./rhizohProductionBootstrap.js\";\nimport { handleAcademicObservatoryExportGetV0 } from \"./rhizoh/academicObservatoryHttpV0.js\";\nimport { handleCouncilAnomalyReasoningPostV0 } from \"./council/rhizohEpistemicCouncilHttpV0.js\";\nimport {\n handleLiveSportsBundleGetV0,\n handleLiveNewsHeadlinesGetV0,\n handleLiveWorldFeedGetV0\n} from \"./rhizoh/liveWorldFeedHttpV0.js\";\nimport {\n buildGenesisRuntimeSurfacePayload,\n recordGenesisEpistemicLedgerPersisted,\n recordGenesisEpistemicSealIssued,\n startGenesisCanonicalClock\n} from \"./genesisRuntimeSurfaceV0.js\";\nimport { registerGenesisSseClient, getGenesisContinuitySeq, setGenesisContinuityAfterPublishHook } from \"./genesisContinuityStreamHubV0.js\";\nimport { computeGenesisReplayFingerprintV0 } from \"./genesisReplayFingerprintV0.js\";\nimport { buildGenesisGatewayCapabilitiesV0 } from \"./genesisGatewayCapabilitiesV0.js\";\nimport {\n installGenesisCheckpointSurfaceGetter,\n noteGenesisCheckpointSeqCommitted,\n getLatestGenesisSignedCheckpoint\n} from \"./genesisContinuityCheckpointV0.js\";\nimport { hydrateGenesisContinuityPersistenceBootV0 } from \"./genesisContinuityHydrateBootV0.js\";\nimport { startGenesisContinuityInfraSampler } from \"./genesisContinuityInfraSamplerV0.js\";\nimport {\n genesisCheckpointQueryBySeqV0,\n genesisCheckpointQueryRangeV0,\n genesisCheckpointQueryLineageV0\n} from \"./genesisCheckpointQueryV0.js\";\nimport {\n genesisContinuityEventArchiveEnabled,\n queryGenesisContinuityEventArchiveV0,\n GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION\n} from \"./genesisContinuityEventArchiveV0.js\";\nimport { resolveGenesisReplayRouterV1, GENESIS_REPLAY_ROUTER_SCHEMA } from \"./genesisReplayRouterV1.js\";\nimport {\n computeGenesisReplayTemporalDiffV1,\n GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA\n} from \"./genesisReplayTemporalDiffV1.js\";\nimport { compareGenesisReplayEquivalenceV1 } from \"./genesisReplayEquivalenceV1.js\";\nimport { computeGenesisReplayAnalyticsV1 } from \"./genesisReplayAnalyticsV1.js\";\nimport { computeGenesisReplayEvolutionV1 } from \"./genesisReplayEvolutionV1.js\";\nimport { handleStripeCheckoutCreate, handleStripeMembershipWebhook } from \"./stripeWebhookMembership.js\";\nimport { buildRealityHealthPayload } from \"./edgeRealityConsistencyV1.js\";\nimport {\n assertWalPeerSocketAuthorityV0,\n buildGatewaySubstrateAuthoritySnapshotV0,\n logGatewaySubstrateAuthorityGuardsV0,\n validateWalPeerFeedSignatureV0\n} from \"./gatewaySubstrateAuthorityV0.js\";\nimport {\n buildSubstrateOperationalSnapshotV0,\n recordWalPeerRejectV0,\n renderSubstratePrometheusMetricsV0\n} from \"./infra/substrateOperationalMetrics.js\";\nimport { ingestClientSubstrateHealthV0 } from \"./rhizohSubstrateHealthIngestV0.js\";\nimport { recordSubstrateOtelEventV0 } from \"./infra/opentelemetrySubstrateV0.js\";\nimport {\n GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n getGenesisWorldObservationIngressStatusV0,\n ingestGenesisWorldObservationV0\n} from \"./genesisContinuityClientIngressV0.js\";\nimport {\n getGenesisContinuitySeqAuditV0,\n noteGenesisContinuitySeqAcceptedV0\n} from \"./genesisContinuitySeqGapV0.js\";\n\n/** Render / Fly / Railway: `PORT` — yoksa `CASTLE_GATEWAY_PORT` — yoksa 8090. */\nconst PORT =\n Number(process.env.PORT) ||\n Number(process.env.CASTLE_GATEWAY_PORT) ||\n 8090;\n/** Spiral / world WebSocket server — set after `wss` init; used by genesis runtime surface. */\nlet spiralWssForGenesis = null;\nconst MAX_MESSAGE_BYTES = Number(process.env.CASTLE_MAX_MESSAGE_BYTES || 32 * 1024);\nconst REQUIRED_GATEWAY_TOKEN = process.env.CASTLE_GATEWAY_TOKEN || \"\";\nconst ALLOWED_ORIGINS = parseAllowedOriginsFromEnv().fromList;\nconst httpCors = createHttpCorsPolicy();\n\n/** @returns {string|null} `Access-Control-Allow-Origin`; null = browser CORS fail. */\nfunction accessControlAllowOriginValue(req) {\n return httpCors.accessControlAllowOriginValue(req);\n}\n\nfunction applyHttpCorsHeaders(req, res) {\n return httpCors.applyHttpCorsHeaders(req, res);\n}\nconst REQUIRE_AUTH = process.env.CASTLE_REQUIRE_AUTH === \"true\";\nconst ALLOW_DEV_ANON = process.env.CASTLE_ALLOW_DEV_ANON !== \"false\";\nconst ALLOW_DEV_HTTP_UID = process.env.CASTLE_ALLOW_DEV_HTTP_UID !== \"false\";\n/** Presence mesh: allow `X-Castle-Guest-Id` when Firebase auth absent (default on). Set `false` to require auth only. */\nconst ALLOW_MESH_GUEST = process.env.CASTLE_ALLOW_MESH_GUEST !== \"false\";\nconst SOCIAL_RETRY_MAX = Math.max(0, Math.min(5, Number(process.env.CASTLE_SOCIAL_RETRY_MAX || 2)));\nconst SOCIAL_RETRY_BASE_MS = Math.max(100, Number(process.env.CASTLE_SOCIAL_RETRY_BASE_MS || 700));\nconst TELEGRAM_BOT_TOKEN = process.env.CASTLE_TELEGRAM_BOT_TOKEN || \"\";\nconst WHATSAPP_TOKEN = process.env.CASTLE_WHATSAPP_TOKEN || \"\";\nconst WHATSAPP_PHONE_NUMBER_ID = process.env.CASTLE_WHATSAPP_PHONE_NUMBER_ID || \"\";\nconst RL_RHIZOH_LLM_PER_MIN = Math.max(5, Number(process.env.CASTLE_RL_RHIZOH_LLM_PER_MIN || 40));\nconst RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN || 30)\n);\nconst RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN || 120));\nconst RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN = Math.max(\n 10,\n Number(process.env.CASTLE_RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN || 60)\n);\nconst RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN = Math.max(8, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN || 48));\nconst RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN = Math.max(4, Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN || 24));\nconst RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN || 18)\n);\nconst RL_EPISTEMIC_SEAL_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_EPISTEMIC_SEAL_PER_MIN || 120));\nconst RL_EPISTEMIC_LOG_PER_MIN = Math.max(20, Number(process.env.CASTLE_RL_EPISTEMIC_LOG_PER_MIN || 240));\nconst EPISTEMIC_SEAL_SECRET = String(\n process.env.CASTLE_EPISTEMIC_SEAL_SECRET || process.env.CASTLE_GATEWAY_TOKEN || \"\"\n).trim();\nconst RL_LLM_CONN_TEST_PER_MIN = Math.max(2, Number(process.env.CASTLE_RL_LLM_CONNECTION_TEST_PER_MIN || 8));\n\n/** One-shot production warnings: CORS surface, auth toggle, dev bypass flags (observatory deploy hygiene). */\nfunction logProductionObservatorySurfaceGuardsV0() {\n if (String(process.env.NODE_ENV || \"\").toLowerCase() !== \"production\") return;\n const tag = \"[GATEWAY_PROD_GUARD]\";\n const primaryCors = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n const corsListEmpty = ALLOWED_ORIGINS.length === 0;\n if (corsListEmpty && !primaryCors) {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty and CASTLE_HTTP_CORS_ORIGIN empty — production still merges RHIZOH_BROWSER_ORIGINS_V1 defaults; set env for preview/staging hosts`\n );\n } else if (corsListEmpty && primaryCors && primaryCors !== \"*\") {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty — only CASTLE_HTTP_CORS_ORIGIN set; add web.app + firebaseapp.com (and previews) to CASTLE_ALLOWED_ORIGINS if clients use alternate hostnames`\n );\n }\n if (primaryCors && /localhost|127\\.0\\.0\\.1/i.test(primaryCors)) {\n console.warn(`${tag} CORS: CASTLE_HTTP_CORS_ORIGIN is localhost while NODE_ENV=production`);\n }\n if (REQUIRE_AUTH) {\n console.warn(\n `${tag} AUTH: CASTLE_REQUIRE_AUTH=true — clients must send a valid gateway credential (otherwise silent 401 on replay/SSE); EventSource cannot set custom headers unless proxied`\n );\n if (REQUIRED_GATEWAY_TOKEN.length < 16) {\n console.warn(`${tag} AUTH: CASTLE_GATEWAY_TOKEN missing or short while require-auth enabled`);\n }\n }\n if (ALLOW_DEV_ANON) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_ANON is not \"false\" — dev anonymous path may be live in production (set CASTLE_ALLOW_DEV_ANON=false)`\n );\n }\n if (ALLOW_DEV_HTTP_UID) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_HTTP_UID is not \"false\" — X-Castle-Dev-Uid bypass risk (set CASTLE_ALLOW_DEV_HTTP_UID=false)`\n );\n }\n}\n\nconst rhizohRuntime = initRhizoh();\n\n/** İstemci: llmKeySource veya keySource — env | user_connection | auto */\nfunction normalizeClientLlmKeySource(raw) {\n const s = String(raw ?? \"\").trim().toLowerCase();\n if (s === \"env\" || s === \"server\") return \"env\";\n if (s === \"user_connection\" || s === \"connection\" || s === \"user\") return \"user_connection\";\n if (s === \"auto\" || s === \"\") return \"auto\";\n return null;\n}\n\n/**\n * Server-side provider wins for env/auto unless user_connection — client bundle may still send provider=openai.\n * @param {\"env\"|\"user_connection\"|\"auto\"} keyMode\n * @param {{ provider?: string, model?: string, apiKey?: string } | null} conn\n * @param {{ provider?: string, model?: string }} payload\n */\nfunction resolveServerLlmProviderV0(keyMode, conn, payload) {\n const envProvider = String(process.env.CASTLE_LLM_PROVIDER || \"\").trim() || undefined;\n const envModel = String(process.env.CASTLE_LLM_MODEL || \"\").trim() || undefined;\n\n if (keyMode === \"user_connection\") {\n return {\n resolvedProvider: conn?.provider || payload?.provider,\n resolvedModel: conn?.model || payload?.model,\n connApiKey: conn?.apiKey || \"\"\n };\n }\n\n if (keyMode === \"env\") {\n return {\n resolvedProvider: envProvider ?? payload?.provider,\n resolvedModel: envModel ?? payload?.model,\n connApiKey: \"\"\n };\n }\n\n if (conn?.provider && conn?.apiKey) {\n return {\n resolvedProvider: conn.provider,\n resolvedModel: conn.model,\n connApiKey: conn.apiKey\n };\n }\n\n return {\n resolvedProvider: envProvider ?? payload?.provider,\n resolvedModel: envModel ?? payload?.model,\n connApiKey: \"\"\n };\n}\n\nfunction jsonReplacerSafeV0(_key, value) {\n if (typeof value === \"bigint\") return Number(value);\n return value;\n}\n\nfunction sendJson(res, statusCode, payload) {\n res.writeHead(statusCode, { \"Content-Type\": \"application/json; charset=utf-8\" });\n try {\n res.end(JSON.stringify(payload, jsonReplacerSafeV0));\n } catch (error) {\n res.end(\n JSON.stringify({\n ok: false,\n error: \"response_serialize_failed\",\n detail: String(error?.message || error)\n })\n );\n }\n}\n\nfunction logRhizohHealth(stage, detail = {}) {\n try {\n console.info(`[CASTLE_${String(stage || \"unknown\")}]`, detail && typeof detail === \"object\" ? detail : {});\n } catch {\n /* noop */\n }\n}\n\nfunction renderInfraMetricsProm() {\n const lines = [\n `castle_gateway_events_processed_total ${infraMetrics.eventsProcessed}`,\n `castle_gateway_duplicate_reject_total ${infraMetrics.duplicateRejects}`,\n `castle_gateway_enqueue_latency_ms ${infraMetrics.enqueueLatencyMs}`,\n `castle_gateway_queue_depth ${infraMetrics.queueDepth}`,\n `castle_gateway_queue_lag_seconds ${(Number(infraMetrics.queueLag || 0) / 1000).toFixed(3)}`,\n `castle_gateway_errors_total ${infraMetrics.errors}`\n ];\n const buckets = infraMetrics.enqueueLatencyBuckets || {};\n for (const k of Object.keys(buckets)) {\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"${k}\"} ${buckets[k]}`);\n }\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"+Inf\"} ${infraMetrics.eventsProcessed}`);\n if (process.env.CASTLE_SUBSTRATE_METRICS !== \"0\") {\n lines.push(renderSubstratePrometheusMetricsV0().trimEnd());\n }\n return lines.join(\"\\n\");\n}\n\nasync function fetchJsonWithTimeout(url, timeoutMs = 1200) {\n const ctl = new AbortController();\n const tid = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const res = await fetch(url, { signal: ctl.signal });\n if (!res.ok) return null;\n return await res.json();\n } catch {\n return null;\n } finally {\n clearTimeout(tid);\n }\n}\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction getHttpPathname(req) {\n let p = String(req.url || \"\").split(\"?\")[0];\n /* Reverse proxies / clients sometimes append `/` — avoid silent 404 on e.g. `/rhizoh/genesis/stream/`. */\n while (p.length > 1 && p.endsWith(\"/\")) {\n p = p.slice(0, -1);\n }\n return p;\n}\n\nfunction rhizohLlmEnvConfigured() {\n const p = String(process.env.CASTLE_LLM_PROVIDER || \"openai\").toLowerCase();\n const key =\n p === \"anthropic\"\n ? process.env.ANTHROPIC_API_KEY\n : p === \"gemini\"\n ? process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY\n : p === \"xai\"\n ? process.env.XAI_API_KEY\n : p === \"deepseek\"\n ? process.env.DEEPSEEK_API_KEY\n : p === \"mistral\"\n ? process.env.MISTRAL_API_KEY\n : p === \"openrouter\"\n ? process.env.OPENROUTER_API_KEY\n : process.env.OPENAI_API_KEY;\n return !!String(key || \"\").trim();\n}\n\n/**\n * @param {import(\"firebase-admin/firestore\").Firestore | null} db\n * @param {number} timeoutMs\n */\nasync function probeFirestoreReachable(db, timeoutMs = 2000) {\n if (!db) return { ok: false, reason: \"no_db\" };\n const timeout = sleep(timeoutMs).then(() => ({ ok: false, reason: \"timeout\" }));\n const work = db\n .collection(\"_castle_gateway_health\")\n .doc(\"ping\")\n .get()\n .then(() => ({ ok: true, reason: \"reachable\" }))\n .catch((e) => ({ ok: false, reason: String(e?.message || e || \"firestore_error\") }));\n return Promise.race([work, timeout]);\n}\n\nfunction stripHtml(text = \"\") {\n return String(text)\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \" \")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \" \")\n .replace(/<[^>]+>/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction haversineMeters(aLat, aLon, bLat, bLon) {\n const R = 6371000;\n const dLat = ((bLat - aLat) * Math.PI) / 180;\n const dLon = ((bLon - aLon) * Math.PI) / 180;\n const sa = Math.sin(dLat / 2);\n const sb = Math.sin(dLon / 2);\n const x = sa * sa + Math.cos((aLat * Math.PI) / 180) * Math.cos((bLat * Math.PI) / 180) * sb * sb;\n return 2 * R * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));\n}\n\nasync function checkPublishTargetHealth(protocol, target) {\n const p = String(protocol || \"\").toUpperCase();\n const t = String(target || \"\").trim();\n if (!t) return { ok: false, reason: \"target_missing\", shouldReconnect: false };\n if (p === \"RTMP\") {\n const ok = t.startsWith(\"rtmp://\") || t.startsWith(\"rtmps://\");\n return { ok, reason: ok ? \"rtmp_target_format_ok\" : \"invalid_rtmp_target\", shouldReconnect: !ok, reconnectInSec: 15 };\n }\n if (p === \"WHIP\" || p === \"SFU\") {\n try {\n const res = await fetch(t, { method: \"OPTIONS\" });\n const ok = res.status < 500;\n return { ok, reason: `http_${res.status}`, shouldReconnect: !ok, reconnectInSec: 8 };\n } catch {\n return { ok: false, reason: \"network_unreachable\", shouldReconnect: true, reconnectInSec: 8 };\n }\n }\n return { ok: true, reason: \"unchecked_protocol\", shouldReconnect: false };\n}\n\nasync function fetchTextFromUrl(url) {\n const res = await fetch(url, { method: \"GET\" });\n if (!res.ok) throw new Error(`fetch_${res.status}`);\n const ct = String(res.headers.get(\"content-type\") || \"\");\n if (ct.includes(\"application/pdf\")) throw new Error(\"pdf_binary_not_supported_directly\");\n const body = await res.text();\n return stripHtml(body).slice(0, 14000);\n}\n\nfunction buildTaskActionGraph(task = \"\", context = {}) {\n const t = String(task || \"\").toLowerCase();\n if (t.includes(\"dinner\") || t.includes(\"akşam yemeği\") || t.includes(\"aksam yemegi\")) {\n return {\n taskTitle: \"Dinner Prep Planner\",\n nodes: [\n { id: \"n1\", title: \"Kitchen preheat\", action: \"smart-oven.preheat\", params: { temperatureC: 190, mode: \"fan\" } },\n { id: \"n2\", title: \"Lighting comfort\", action: \"smart-light.set_scene\", params: { scene: \"warm_dinner\" } },\n { id: \"n3\", title: \"Fridge inventory check\", action: \"smart-fridge.inventory_scan\", params: {} },\n { id: \"n4\", title: \"Reminder notification\", action: \"smart-watch.notify\", params: { text: \"Dinner prep started.\" } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" },\n { from: \"n3\", to: \"n4\" }\n ]\n };\n }\n return {\n taskTitle: task || \"General Planner\",\n nodes: [\n { id: \"n1\", title: \"Safety check\", action: \"device.safety_check\", params: { context } },\n { id: \"n2\", title: \"Primary action\", action: \"device.execute_task\", params: { task } },\n { id: \"n3\", title: \"Report\", action: \"studio.timeline_note\", params: { task } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" }\n ]\n };\n}\n\nfunction buildEthicsPrograms() {\n return {\n policyVersion: \"v1\",\n tracks: [\n {\n id: \"elderly-care-assist\",\n audience: \"elderly\",\n title: \"Elderly Assistance Track\",\n principles: [\"human-in-the-loop\", \"consent-first\", \"low-friction alerts\"],\n modules: [\"medication-reminder\", \"fall-risk-check\", \"family-notify\", \"daily-companion-brief\"]\n },\n {\n id: \"kids-safe-learning\",\n audience: \"children\",\n title: \"Kids Safe Learning Track\",\n principles: [\"guardian-consent\", \"age-appropriate-language\", \"content-filtering\"],\n modules: [\"study-coach\", \"screen-time-balance\", \"safe-social-share\", \"emergency-contact\"]\n }\n ]\n };\n}\n\nasync function deliverViaTelegram(channel, text) {\n const chatId = String(channel.endpoint || \"\").trim();\n if (!TELEGRAM_BOT_TOKEN) throw new Error(\"telegram_token_missing\");\n if (!chatId) throw new Error(\"telegram_chat_id_missing\");\n const url = `https://api.telegram.org/bot${encodeURIComponent(TELEGRAM_BOT_TOKEN)}/sendMessage`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ chat_id: chatId, text })\n });\n if (!res.ok) throw new Error(`telegram_http_${res.status}`);\n return { provider: \"telegram\", status: res.status };\n}\n\nasync function deliverViaWhatsapp(channel, text) {\n const to = String(channel.endpoint || \"\").trim();\n if (!WHATSAPP_TOKEN) throw new Error(\"whatsapp_token_missing\");\n if (!WHATSAPP_PHONE_NUMBER_ID) throw new Error(\"whatsapp_phone_number_id_missing\");\n if (!to) throw new Error(\"whatsapp_to_missing\");\n const url = `https://graph.facebook.com/v22.0/${encodeURIComponent(WHATSAPP_PHONE_NUMBER_ID)}/messages`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${WHATSAPP_TOKEN}` },\n body: JSON.stringify({ messaging_product: \"whatsapp\", to, type: \"text\", text: { body: text } })\n });\n if (!res.ok) throw new Error(`whatsapp_http_${res.status}`);\n return { provider: \"whatsapp\", status: res.status };\n}\n\nasync function deliverViaWebhook(channel, text) {\n const endpoint = String(channel.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"webhook_endpoint_missing\");\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text, platform: channel.platform || \"webhook\", ts: Date.now() })\n });\n if (!res.ok) throw new Error(`webhook_http_${res.status}`);\n return { provider: \"webhook\", status: res.status };\n}\n\nasync function deliverSocialMessage(channel, text) {\n const platform = String(channel.platform || \"webhook\").toLowerCase();\n if (platform === \"telegram\") return deliverViaTelegram(channel, text);\n if (platform === \"whatsapp\") return deliverViaWhatsapp(channel, text);\n return deliverViaWebhook(channel, text);\n}\n\nasync function deliverSocialWithRetry(channel, text) {\n let attempt = 0;\n let lastError = null;\n while (attempt <= SOCIAL_RETRY_MAX) {\n try {\n const result = await deliverSocialMessage(channel, text);\n return { ok: true, attempt: attempt + 1, ...result };\n } catch (error) {\n lastError = error;\n if (attempt >= SOCIAL_RETRY_MAX) break;\n const jitter = Math.floor(Math.random() * 90);\n await sleep(SOCIAL_RETRY_BASE_MS * Math.pow(2, attempt) + jitter);\n }\n attempt += 1;\n }\n return { ok: false, attempt: attempt + 1, error: String(lastError?.message || \"delivery_failed\") };\n}\n\nasync function dispatchCommandToAdapter(device, command) {\n const adapter = String(command?.adapter || device?.adapter || \"websocket\").toLowerCase();\n const endpoint = String(device?.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"device_endpoint_missing\");\n if (adapter === \"mqtt\") {\n // MQTT delivery is proxied via a bridge endpoint (HTTP webhook style).\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n type: \"mqtt-command\",\n topic: `castle/devices/${device.id}/commands`,\n action: command.action,\n params: command.params || {}\n })\n });\n if (!res.ok) throw new Error(`mqtt_bridge_http_${res.status}`);\n return { ok: true, adapter, via: \"http-bridge\", status: res.status };\n }\n if (!endpoint.startsWith(\"ws://\") && !endpoint.startsWith(\"wss://\")) throw new Error(\"invalid_websocket_endpoint\");\n const sendResult = await new Promise((resolve, reject) => {\n const ws = new WebSocket(endpoint);\n const timer = setTimeout(() => {\n try {\n ws.close();\n } catch {\n /* noop */\n }\n reject(new Error(\"websocket_timeout\"));\n }, 3000);\n ws.on(\"open\", () => {\n ws.send(\n JSON.stringify({\n type: \"castle-command\",\n commandId: command.id,\n deviceId: command.deviceId,\n action: command.action,\n params: command.params || {}\n }),\n (err) => {\n clearTimeout(timer);\n ws.close();\n if (err) reject(new Error(\"websocket_send_failed\"));\n else resolve({ ok: true, adapter: \"websocket\", via: \"ws\" });\n }\n );\n });\n ws.on(\"error\", () => {\n clearTimeout(timer);\n reject(new Error(\"websocket_connect_failed\"));\n });\n });\n return sendResult;\n}\n\nfunction readHttpJson(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => {\n if (!buf) return resolve({});\n const payload = safeJsonParse(buf);\n if (!payload || typeof payload !== \"object\") return reject(new Error(\"invalid_json_payload\"));\n resolve(payload);\n });\n req.on(\"error\", reject);\n });\n}\n\n/** Ham gövde (HMAC doğrulaması için). */\nfunction readHttpBodyText(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => resolve(buf));\n req.on(\"error\", reject);\n });\n}\n\nfunction maskOpaqueId(value) {\n const t = String(value || \"\");\n if (!t) return \"(empty)\";\n if (t.length <= 8) return \"****\";\n return `${t.slice(0, 4)}…${t.slice(-2)}`;\n}\n\nfunction readGatewayHttpToken(req) {\n const x = String(req.headers?.[\"x-castle-gateway-token\"] || \"\").trim();\n if (x) return x;\n const auth = String(req.headers?.authorization || \"\");\n const m = /^Bearer\\s+(.+)$/i.exec(auth);\n return m ? String(m[1] || \"\").trim() : \"\";\n}\n\nasync function resolveHttpUser(req) {\n const authResult = await verifyClientToken(req);\n if (authResult?.ok && authResult.user?.uid) {\n return { ok: true, uid: String(authResult.user.uid), mode: authResult.kind || \"auth\" };\n }\n if (ALLOW_DEV_ANON && ALLOW_DEV_HTTP_UID) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) return { ok: true, uid: `dev-${devUid.slice(0, 64)}`, mode: \"dev-header\" };\n }\n return { ok: false, reason: authResult?.reason || \"auth_required\" };\n}\n\n/**\n * Epistemic ledger ingest — Firebase uid OR valid transport token + X-Castle-Dev-Uid.\n * Prod disables CASTLE_ALLOW_DEV_HTTP_UID; LLM may still work while telemetry 401'd without this.\n */\nasync function resolveEpistemicIngestActor(req) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok && tok === REQUIRED_GATEWAY_TOKEN) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) {\n return { ok: true, uid: `gw-telemetry-${devUid.slice(0, 64)}`, mode: \"gateway-token-dev-uid\" };\n }\n return { ok: false, reason: \"dev_uid_required\" };\n }\n if (tok) {\n return { ok: false, reason: \"gateway_token_mismatch\" };\n }\n }\n const auth = await resolveHttpUser(req);\n if (auth.ok) return auth;\n return { ok: false, reason: auth.reason || \"auth_required\" };\n}\n\n/** Mesh actor: Firebase uid, dev header, or `guest:<uuid>` via header / `guestId` query (SSE cannot set headers). */\nasync function resolveMeshActor(req) {\n const auth = await resolveHttpUser(req);\n if (auth.ok) return { ok: true, clientUid: auth.uid, mode: auth.mode };\n if (ALLOW_MESH_GUEST) {\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const gid = String(req.headers?.[\"x-castle-guest-id\"] || q.searchParams.get(\"guestId\") || \"\").trim();\n if (gid) {\n const normalized = gid.startsWith(\"guest:\") ? gid.slice(0, 180) : `guest:${gid.slice(0, 160)}`;\n return { ok: true, clientUid: normalized, mode: \"guest-header\" };\n }\n }\n return { ok: false, reason: \"mesh_auth_required\" };\n}\n\n/** Live genesis runtime JSON (GET /rhizoh/genesis/runtime + SSE infra sampler). */\nasync function buildGenesisRuntimeSurfacePayloadLive() {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n let spiralWsActive = 0;\n if (spiralWssForGenesis) {\n for (const c of spiralWssForGenesis.clients) {\n if (c.readyState === 1) spiralWsActive += 1;\n }\n }\n const base = buildGenesisRuntimeSurfacePayload({\n infraMetrics,\n scoredHealth: scored,\n rhizohEnterpriseMetrics,\n mesh: meshContinuityAggregate(),\n workerHealth: worker,\n port: PORT,\n spiralWebSocketClientsActive: spiralWsActive\n });\n return {\n ...base,\n replayFingerprint: computeGenesisReplayFingerprintV0(base),\n gatewayCapabilities: buildGenesisGatewayCapabilitiesV0(),\n genesisStream: {\n schema: \"castle.genesis.stream_cursor.v0\",\n lastAcceptedSeq: getGenesisContinuitySeq(),\n clientIngress: {\n schema: GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n contract: \"castle.world_observation.ingress_envelope.v1\",\n postPath: rhizohRuntime.routes.genesisIngress,\n closure: getGenesisWorldObservationIngressStatusV0()\n },\n seqContinuity: getGenesisContinuitySeqAuditV0(),\n eventArchive: genesisContinuityEventArchiveEnabled()\n ? {\n enabled: true,\n projection: GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION,\n queryPath: rhizohRuntime.routes.genesisContinuityEvents\n }\n : { enabled: false },\n replayRouter: {\n schema: GENESIS_REPLAY_ROUTER_SCHEMA,\n queryPath: rhizohRuntime.routes.genesisReplay,\n queryHint: \"from=&to= or range=from-to; optional type=&checkpoints=0\",\n temporalDiffPath: rhizohRuntime.routes.genesisReplayDiff,\n temporalDiffSchema: GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA,\n equivalencePath: rhizohRuntime.routes.genesisReplayEquivalence,\n analyticsPath: rhizohRuntime.routes.genesisReplayAnalytics,\n evolutionPath: rhizohRuntime.routes.genesisReplayEvolution\n }\n }\n };\n}\n\nconst httpServer = createServer(async (req, res) => {\n const corsAllow = applyHttpCorsHeaders(req, res);\n if (req.method === \"OPTIONS\") {\n if (!corsAllow && req.headers?.origin) {\n console.warn(\"[GATEWAY_CORS] preflight rejected for origin:\", req.headers.origin);\n }\n res.writeHead(corsAllow ? 204 : 403);\n res.end();\n return;\n }\n\n const pathname = getHttpPathname(req);\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/genesis/__ping\") {\n sendJson(res, 200, {\n ok: true,\n pid: process.pid,\n entry: __filename,\n time: Date.now(),\n genesis: true\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/epistemic/__probe\") {\n sendJson(res, 200, {\n ok: true,\n epistemicIngestActorV2: true,\n routes: {\n seal: rhizohRuntime.routes.epistemicSeal,\n logsBatch: rhizohRuntime.routes.epistemicLogsBatch,\n authorityLedgerBatch: rhizohRuntime.routes.authorityLedgerBatch,\n authorityLedgerReplay: rhizohRuntime.routes.authorityLedgerReplay,\n authorityEpochMerge: rhizohRuntime.routes.authorityEpochMerge\n },\n hasGatewayToken: Boolean(REQUIRED_GATEWAY_TOKEN),\n gatewayTokenLen: REQUIRED_GATEWAY_TOKEN ? REQUIRED_GATEWAY_TOKEN.length : 0\n });\n return;\n }\n\n if (\n req.method === \"GET\" &&\n (pathname === \"/rhizoh/live/world-feed\" ||\n pathname === \"/rhizoh/live/sports-bundle\" ||\n pathname === \"/rhizoh/live/news-headlines\")\n ) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_live_world_feed:${ip}`, RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const out =\n pathname === \"/rhizoh/live/sports-bundle\"\n ? await handleLiveSportsBundleGetV0()\n : pathname === \"/rhizoh/live/news-headlines\"\n ? await handleLiveNewsHeadlinesGetV0(req)\n : await handleLiveWorldFeedGetV0(req);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/webhooks/stripe\") {\n await handleStripeMembershipWebhook(req, res);\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/billing/stripe/checkout\") {\n await handleStripeCheckoutCreate(req, res, { readHttpJson, resolveHttpUser, sendJson });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/live\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, live: false, dns: true, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n sendJson(res, 200, { ok: true, live: true, dns: true, service: \"castle-gateway\", ts: Date.now() });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/cors\") {\n const reqOrigin = normalizeHttpOrigin(req.headers?.origin);\n const allowOrigin = accessControlAllowOriginValue(req);\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n requestOrigin: reqOrigin || null,\n allowed: Boolean(allowOrigin),\n allowOrigin: allowOrigin || null,\n allowSetSize: httpCors.allowSet.size\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/ready\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, ready: false, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n try {\n const persistence = getFirebasePersistence();\n const llmProbe = rhizohLlmEnvConfigured();\n sendJson(res, 200, {\n ok: true,\n ready: true,\n dns: true,\n persistence: persistence.mode,\n service: \"castle-gateway\",\n gateway: \"ok\",\n llm: llmProbe ? \"ok\" : \"degraded\",\n spine: llmProbe ? \"ok\" : \"partial\"\n });\n } catch (e) {\n sendJson(res, 503, { ok: false, ready: false, reason: String(e?.message || e), service: \"castle-gateway\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/reality\") {\n const payload = buildRealityHealthPayload();\n sendJson(res, payload.ok ? 200 : 503, payload);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/substrate\") {\n const authority = buildGatewaySubstrateAuthoritySnapshotV0();\n const operational = buildSubstrateOperationalSnapshotV0();\n const drift = buildRealityHealthPayload();\n const ok = Boolean(authority.productionAuthoritySatisfied) && drift.ok;\n sendJson(res, ok ? 200 : 503, {\n ok,\n service: \"castle-gateway\",\n ts: Date.now(),\n authority,\n operational,\n drift: { ok: drift.ok, errors: drift.drift?.errors, warnings: drift.drift?.warnings }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.councilAnomalyReasoning) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_council_anomaly:${ip}`, 30, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const body = await readHttpJson(req);\n const out = handleCouncilAnomalyReasoningPostV0(body);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/substrate/health\") {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const body = await readHttpJson(req);\n const uid = (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestClientSubstrateHealthV0(body, { uid, clientId: uid });\n if (!result.ok) {\n sendJson(res, 400, result);\n return;\n }\n recordSubstrateOtelEventV0(\"client_health_ingest\", { uid: String(uid).slice(0, 32) });\n sendJson(res, 200, result);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/deps\") {\n const t0 = Date.now();\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, {\n ok: false,\n dns: true,\n llm: false,\n firestore: false,\n phase: \"maintenance\",\n latencyMs: Date.now() - t0,\n service: \"castle-gateway\"\n });\n return;\n }\n const persistence = getFirebasePersistence();\n let firestoreOk = persistence.mode !== \"firebase\";\n let firestoreDetail = persistence.mode === \"file\" ? \"file_mode\" : \"not_applicable\";\n if (persistence.mode === \"firebase\" && persistence.db) {\n const pr = await probeFirestoreReachable(persistence.db, 2000);\n firestoreOk = pr.ok;\n firestoreDetail = pr.reason || (pr.ok ? \"reachable\" : \"fail\");\n } else if (persistence.mode === \"firebase\" && !persistence.db) {\n firestoreOk = false;\n firestoreDetail = \"firebase_not_initialized\";\n }\n const llmOk = rhizohLlmEnvConfigured();\n const overallOk = llmOk && firestoreOk;\n const latencyMs = Date.now() - t0;\n sendJson(res, 200, {\n ok: overallOk,\n dns: true,\n llm: llmOk,\n firestore: persistence.mode === \"firebase\" ? firestoreOk : false,\n persistence: persistence.mode,\n firestoreDetail,\n latencyMs,\n service: \"castle-gateway\",\n wsPort: PORT\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health\") {\n const persistence = getFirebasePersistence();\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n wsPort: PORT,\n persistence: persistence.mode,\n presenceMesh: \"sse\"\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/health\") {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n sendJson(res, 200, {\n ok: true,\n role: \"gateway\",\n queueLagMs: infraMetrics.queueLag,\n queueDepth: infraMetrics.queueDepth,\n errors: infraMetrics.errors,\n status: scored.status,\n score: scored.score,\n reasons: scored.reasons,\n worker: worker\n ? {\n status: worker.status,\n score: worker.score,\n reasons: worker.reasons\n }\n : null,\n timestamp: Date.now()\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisRuntime) {\n const payload = await buildGenesisRuntimeSurfacePayloadLive();\n sendJson(res, 200, payload);\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.genesisIngress) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const actor = await resolveMeshActor(req);\n const clientId = actor.ok ? actor.clientUid : (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestGenesisWorldObservationV0(body, { clientId });\n if (!result.ok) {\n const status =\n result.error === \"rate_limited\" || result.error === \"agent_spoke_throttled\"\n ? 429\n : result.error === \"payload_too_large\"\n ? 413\n : 400;\n sendJson(res, status, result);\n return;\n }\n sendJson(res, result.idempotent ? 200 : 202, result);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisStream) {\n const corsOrigin = accessControlAllowOriginValue(req);\n if (!corsOrigin && String(process.env.NODE_ENV || \"\").toLowerCase() === \"production\") {\n const og = String(req.headers?.origin || \"\").trim();\n if (og) {\n console.warn(\"[GATEWAY] genesis SSE: missing Access-Control-Allow-Origin for browser Origin (CORS whitelist mismatch):\", og);\n }\n }\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n res.write(\"retry: 3000\\n\\n\");\n res.write(\": genesis continuity stream\\n\\n\");\n res.write(\"event: genesis\\n\");\n res.write(`data: {\"booted\":true,\"pid\":${process.pid}}\\n\\n`);\n registerGenesisSseClient(res, req);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLatest) {\n const cp = getLatestGenesisSignedCheckpoint();\n if (!cp) {\n sendJson(res, 404, { ok: false, error: \"no_checkpoint_yet\", hint: \"wait_for_seq_interval_or_configure_signing_secret\" });\n return;\n }\n sendJson(res, 200, { ok: true, checkpoint: cp });\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(rhizohRuntime.routes.genesisCheckpointBySeqPrefix)) {\n const tail = pathname.slice(rhizohRuntime.routes.genesisCheckpointBySeqPrefix.length).split(\"/\")[0];\n const seq = parseInt(String(tail || \"\").trim(), 10);\n const out = await genesisCheckpointQueryBySeqV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"checkpoint_not_found\") {\n sendJson(res, 404, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointRange) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryRangeV0(from, to);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"range_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLineage) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const seq = parseInt(String(u.searchParams.get(\"seq\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryLineageV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"lineage_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisContinuityEvents) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const limit = parseInt(String(u.searchParams.get(\"limit\") || \"\").trim(), 10);\n const out = await queryGenesisContinuityEventArchiveV0(from, to, type, limit);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\" || out.error === \"event_archive_disabled\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"archive_file_too_large\") {\n sendJson(res, 413, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplay) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await resolveGenesisReplayRouterV1({\n from: fromSeq,\n to: toSeq,\n type,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayDiff) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const out = await computeGenesisReplayTemporalDiffV1({ from: fromSeq, to: toSeq, type });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEquivalence) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from1 = parseInt(String(u.searchParams.get(\"from1\") || \"\").trim(), 10);\n const to1 = parseInt(String(u.searchParams.get(\"to1\") || \"\").trim(), 10);\n const from2 = parseInt(String(u.searchParams.get(\"from2\") || \"\").trim(), 10);\n const to2 = parseInt(String(u.searchParams.get(\"to2\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await compareGenesisReplayEquivalenceV1({\n from1,\n to1,\n from2,\n to2,\n type,\n includeCheckpoints\n });\n if (\n !out.ok &&\n (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")\n ) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayAnalytics) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayAnalyticsV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEvolution) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const collapseWindows = parseInt(String(u.searchParams.get(\"collapseWindows\") || \"0\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayEvolutionV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n collapseWindows,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/metrics\") {\n res.writeHead(200, { \"Content-Type\": \"text/plain; version=0.0.4; charset=utf-8\" });\n res.end(renderInfraMetricsProm());\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.infraPrometheusMetrics) {\n if (process.env.CASTLE_PROMETHEUS_METRICS !== \"1\") {\n sendJson(res, 404, { ok: false, error: \"prometheus_metrics_disabled\" });\n return;\n }\n res.writeHead(200, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(renderRhizohPrometheusMetrics());\n return;\n }\n if (req.method === \"GET\" && pathname.startsWith(\"/infra/traces/\")) {\n const traceId = decodeURIComponent(pathname.slice(\"/infra/traces/\".length));\n const item = await getTraceById(traceId);\n if (!item) return sendJson(res, 404, { ok: false, error: \"trace_not_found\" });\n return sendJson(res, 200, { ok: true, trace: item });\n }\n if (req.method === \"GET\" && pathname === \"/infra/traces\") {\n const u = new URL(String(req.url || \"/infra/traces\"), `http://${req.headers.host || \"localhost\"}`);\n const sessionId = String(u.searchParams.get(\"sessionId\") || \"\").trim();\n const items = sessionId ? await listTracesBySession(sessionId) : [];\n return sendJson(res, 200, { ok: true, sessionId, traces: items });\n }\n\n /** Presence room mesh (C): JOIN / LEAVE / SNAPSHOT / DELTA / REPLAY (HTTP); SUBSCRIBE = SSE stream. */\n if (req.method === \"POST\" && pathname === \"/presence/mesh/join\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid, clientUid: actor.clientUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/leave\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshLeave(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/snapshot\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n sendJson(res, 200, meshSnapshot(roomUid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/delta\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, MAX_MESSAGE_BYTES);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const seq = meshAppendDelta(roomUid, actor.clientUid, {\n node: body?.node,\n projectionPatch: body?.projectionPatch,\n writerSubject: body?.writerSubject\n });\n sendJson(res, 200, { ok: true, roomUid, seq });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/replay\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const fromNodeId = body?.fromNodeId != null ? String(body.fromNodeId) : \"\";\n const fromSeq = body?.fromSeq;\n if (fromNodeId) {\n sendJson(res, 200, meshReplayFromNodeId(roomUid, fromNodeId));\n return;\n }\n sendJson(res, 200, meshReplayFromSeq(roomUid, fromSeq));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/presence/mesh/subscribe\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const roomUid = String(q.searchParams.get(\"roomUid\") || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n const write = (chunk) => {\n try {\n res.write(chunk);\n } catch {\n /* closed */\n }\n };\n const unsub = meshSubscribeSse(roomUid, write);\n const onClose = () => {\n unsub();\n meshLeave(roomUid, actor.clientUid);\n try {\n res.end();\n } catch {\n /* ignore */\n }\n };\n req.on(\"close\", onClose);\n req.on(\"aborted\", onClose);\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, uid: auth.uid, mode: auth.mode, items: await listConnections(auth.uid) });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const id = await createConnection(auth.uid, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"create_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n await updateConnection(auth.uid, id, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"update_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"DELETE\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n await deleteConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"delete_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url?.startsWith(\"/rhizoh/network/presence\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const u = new URL(req.url, \"http://localhost\");\n const roomKey = String(u.searchParams.get(\"room\") || \"world_space_c2c_v0\").slice(0, 64);\n const sessionId = String(u.searchParams.get(\"sessionId\") || \"\").trim();\n const unified = listUnifiedGatewayPresenceV0({ roomKey, sessionId: sessionId || undefined });\n sendJson(res, 200, {\n ok: true,\n roomKey,\n sessionId: sessionId || null,\n presence: unified,\n castlePeers: unified.castlePeers,\n matchMembers: unified.matchMembers,\n services: unified.services\n });\n } catch (error) {\n sendJson(res, 500, { ok: false, error: error?.message || \"presence_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/rhizoh/sync/vault\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const snapshot = await getCloudSyncSnapshotV0(auth.uid);\n sendJson(res, 200, { ok: true, uid: auth.uid, snapshot });\n } catch (error) {\n sendJson(res, 500, { ok: false, error: error?.message || \"cloud_sync_pull_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/rhizoh/sync/vault\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const snapshot = await mergeCloudSyncSnapshotV0(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, uid: auth.uid, snapshot });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"cloud_sync_push_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections/test\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n if (!checkHttpRateLimit(`llm_conn_test:${auth.uid}`, RL_LLM_CONN_TEST_PER_MIN, 60_000)) {\n return sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n }\n const hasBodyKey = !!String(payload?.apiKey || \"\").trim();\n const result = await queryRhizohLlm(\n {\n message: String(payload?.message || \"Ping from Castle Gateway\"),\n context: payload?.context || { source: \"connection_test\" },\n provider: payload?.provider,\n model: payload?.model,\n apiKey: payload?.apiKey || \"\"\n },\n { allowExternalApiKey: true, llmKeySource: hasBodyKey ? \"auto\" : \"env\" }\n );\n logLlmAccess({\n route: \"/llm/connections/test\",\n uid: auth.uid,\n llmKeyBillingOwner: result.llmKeyBillingOwner,\n llmKeyOrigin: result.llmKeyOrigin,\n provider: result.provider,\n model: result.model,\n connectionId: null\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"connection_test_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAgentIdentities(auth.uid);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerAgentIdentity({\n ownerUid: auth.uid,\n agentId: payload?.agentId,\n role: payload?.role,\n personaSeed: payload?.personaSeed || {},\n capabilityLevel: payload?.capabilityLevel\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/agents/identities/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const owns = await getAgentIdentity(id);\n if (!owns || owns.ownerUid !== auth.uid) return sendJson(res, 403, { ok: false, error: \"forbidden\" });\n const payload = await readHttpJson(req);\n const row = await updateAgentIdentity(id, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/academy/events\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/academy/events\"}`);\n const status = String(url.searchParams.get(\"status\") || \"\");\n const items = await listAcademyEvents({ ownerUid: auth.uid, status, limit: 120 });\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/academy/events\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const ev = await queueAcademyEvent({\n type: payload?.type,\n roomId: payload?.roomId,\n ownerUid: auth.uid,\n participants: payload?.participants || [],\n topic: payload?.topic || \"academy\",\n payload: payload?.payload || {}\n });\n sendJson(res, 200, { ok: true, event: ev });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && String(req.url || \"\").startsWith(\"/academy/events/\") && String(req.url || \"\").endsWith(\"/resolve\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const parts = String(req.url || \"\").split(\"/\").filter(Boolean);\n const eventId = parts[2] || \"\";\n try {\n const payload = await readHttpJson(req);\n const out = await resolveAcademyEvent(eventId, payload || {});\n sendJson(res, 200, { ok: true, event: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_resolve_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/memory\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/memory\"}`);\n const scope = url.searchParams.get(\"scope\") === \"agents\" ? \"agents\" : \"users\";\n const id = String(url.searchParams.get(\"id\") || (scope === \"agents\" ? \"\" : auth.uid));\n const kind = String(url.searchParams.get(\"kind\") || \"\");\n const limit = Number(url.searchParams.get(\"limit\") || 40);\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n sendJson(res, 200, { ok: true, scope, id, kind, items: await listMemories({ scope, id, kind, limit }) });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const profile = await getPersonaGoalMemory(auth.uid);\n sendJson(res, 200, { ok: true, profile });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const profile = await setPersonaGoalMemory({\n uid: auth.uid,\n personaPatch: payload?.personaPatch || {},\n goals: Array.isArray(payload?.goals) ? payload.goals : null,\n preferencesPatch: payload?.preferencesPatch || {}\n });\n sendJson(res, 200, { ok: true, profile });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"profile_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/compact\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const out = await autoCompactMemories({ scope, id });\n sendJson(res, 200, { ok: true, scope, id, ...out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_compact_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/context\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const context = await getMemoryContext({\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n query: String(payload?.query || \"\"),\n limit: Number(payload?.limit || 80)\n });\n sendJson(res, 200, { ok: true, context });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_context_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/place-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const placeName = String(payload?.placeName || \"\").trim();\n if (!placeName) return sendJson(res, 400, { ok: false, error: \"placeName_required\" });\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const persona = String(payload?.waypointPersona || payload?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(placeName)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Place: ${placeName}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Give a concise conversational briefing about this place for a walking-route companion.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-place-brief\", placeName, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `PLACE_BRIEF:${placeName} => ${String(summary.reply || \"\").slice(0, 900)}`,\n tags: [\"place\", \"brief\", \"event-layer\"],\n importance: 0.62,\n meta: { placeName }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"place-brief\",\n text: `${placeName} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: {\n placeName,\n directive: summary.directive,\n lat: hit?.lat ? Number(hit.lat) : null,\n lon: hit?.lon ? Number(hit.lon) : null\n }\n });\n sendJson(res, 200, { ok: true, place: hit || null, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"place_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/route-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const waypoints = Array.isArray(payload?.waypoints) ? payload.waypoints.slice(0, 24) : [];\n const out = [];\n for (const wp of waypoints) {\n const name = String(wp?.name || \"\").trim();\n if (!name) continue;\n const persona = String(wp?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(name)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Waypoint: ${name}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Generate a concise spoken briefing for route waypoint.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-route-brief\", waypoint: name, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n out.push({\n name,\n persona,\n place: hit || null,\n reply: summary.reply,\n directive: summary.directive\n });\n }\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `ROUTE_BRIEF: waypoints=${out.length} ${out.map((x) => `${x.name}:${x.persona}`).join(\" | \")}`.slice(0, 1300),\n tags: [\"route\", \"waypoint\", \"brief\", \"event-layer\"],\n importance: 0.74,\n meta: { count: out.length }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"route-brief\",\n text: out.map((x) => `${x.name}:${String(x.reply || \"\").slice(0, 140)}`).join(\" | \").slice(0, 1400),\n roomId: \"studio-main\",\n meta: {\n waypointCount: out.length,\n waypoints: out.map((x) => ({\n name: x.name,\n persona: x.persona,\n lat: x.place?.lat ? Number(x.place.lat) : null,\n lon: x.place?.lon ? Number(x.place.lon) : null\n }))\n }\n });\n sendJson(res, 200, { ok: true, items: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"route_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/pdf-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const title = String(payload?.title || \"document\");\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const providedText = String(payload?.text || \"\");\n const url = String(payload?.url || \"\");\n let content = providedText;\n if (!content && url) content = await fetchTextFromUrl(url);\n if (!content) return sendJson(res, 400, { ok: false, error: \"text_or_supported_url_required\" });\n const summary = await queryRhizohLlm(\n {\n message: `Summarize this document and provide key points + practical actions.\\nTitle:${title}\\nContent:\\n${content.slice(0, 12000)}`,\n context: { source: \"event-layer-pdf-brief\", title },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `DOC_BRIEF:${title} => ${String(summary.reply || \"\").slice(0, 1000)}`,\n tags: [\"document\", \"brief\", \"library\", \"event-layer\"],\n importance: 0.7,\n meta: { title, url: url || null }\n });\n await addTranscript(auth.uid, {\n source: \"library-brief\",\n eventType: \"pdf-brief\",\n text: `${title} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { title }\n });\n sendJson(res, 200, { ok: true, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/pdf/upload\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req, 8 * 1024 * 1024);\n const job = await enqueuePdfJob({\n uid: auth.uid,\n fileName: payload?.fileName || \"document.pdf\",\n bytesBase64: payload?.contentBase64\n });\n sendJson(res, 200, { ok: true, job });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_upload_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/session\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createPublishSession(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session\",\n text: `Publish session armed: ${row.protocol} ${row.target}`.slice(0, 500),\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: row.id, bridge: row.bridge }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/studio/publish/sessions\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listPublishSessions(auth.uid, 80);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/health-check\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const protocol = String(payload?.protocol || \"WHIP\");\n const target = String(payload?.target || \"\");\n const health = await checkPublishTargetHealth(protocol, target);\n if (payload?.sessionId) {\n await updatePublishSession(auth.uid, String(payload.sessionId), {\n status: health.ok ? \"healthy\" : \"degraded\",\n reconnectPolicy: {\n enabled: true,\n backoffSec: [2, 5, 10, 20],\n nextRetryInSec: health.reconnectInSec || 0,\n maxRetries: 8,\n reason: health.reason\n }\n });\n }\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-health\",\n text: `${protocol} ${target} => ${health.ok ? \"healthy\" : \"degraded\"} (${health.reason})`,\n roomId: \"studio-main\",\n meta: { protocol, target, reconnect: !!health.shouldReconnect }\n });\n sendJson(res, 200, { ok: true, health });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_health_check_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/studio/publish/sessions/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updatePublishSession(auth.uid, id, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session-update\",\n text: `Session ${id} -> ${row.status || \"updated\"}`,\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: id, status: row.status, ingestUrl: row.ingestUrl || \"\" }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/transcripts\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/transcripts\"}`);\n const limit = Math.max(1, Math.min(200, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await listTranscripts(auth.uid, limit);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/transcripts\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await addTranscript(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"transcript_write_failed\" });\n }\n return;\n }\n\n /** E1: broadcast intent → GreenRoom capability → transcript + memory artifact */\n if (req.method === \"POST\" && req.url === \"/studio/capabilities/greenroom\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const traceId = String(payload?.traceId || `TRC-${Date.now().toString(36).toUpperCase()}`);\n const title = String(payload?.title || \"Castle Live Broadcast\").slice(0, 200);\n const intentRaw = String(payload?.intentRaw || \"\").slice(0, 800);\n const audienceEstimate = Math.max(0, Math.min(50_000_000, Number(payload?.audienceEstimate) || 1200));\n const roomId = String(payload?.roomId || \"greenroom-main\");\n const transcriptRow = await addTranscript(auth.uid, {\n source: \"rhizoh\",\n eventType: \"BROADCAST_ROUTED\",\n text: `GreenRoom · ${title}${intentRaw ? `\\nIntent: ${intentRaw}` : \"\"}`,\n roomId,\n meta: {\n ack: \"BROADCAST_ROUTED\",\n capability: \"greenroom\",\n intentType: \"broadcast\",\n traceId,\n audienceEstimate,\n artifact: {\n kind: \"broadcast_card\",\n title,\n traceId,\n roomId,\n createdAt: Date.now()\n }\n }\n });\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n text: `GREENROOM_BROADCAST:${title} trace=${traceId}`,\n tags: [\"broadcast\", \"greenroom\", \"artifact\", \"castle-library\"],\n importance: 0.72,\n kind: \"episodic\",\n meta: { traceId, capability: \"greenroom\", transcriptId: transcriptRow.id }\n });\n sendJson(res, 200, {\n ok: true,\n ack: \"BROADCAST_ROUTED\",\n traceId,\n transcript: transcriptRow,\n replayPath: `/replay/${encodeURIComponent(traceId)}`,\n sharePath: `/replay/${encodeURIComponent(traceId)}`\n });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"greenroom_capability_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/metadata/search\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/metadata/search\"}`);\n const q = String(url.searchParams.get(\"q\") || \"\");\n const limit = Math.max(1, Math.min(300, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await searchMetadata(auth.uid, q, limit);\n sendJson(res, 200, { ok: true, q, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerDevice(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, device: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"device_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const lat = Number(payload?.lat);\n const lon = Number(payload?.lon);\n const speed = Math.max(0, Number(payload?.speed || 0));\n const cfgCenterLat = Number(process.env.CASTLE_GEOFENCE_CENTER_LAT || 41.0082);\n const cfgCenterLon = Number(process.env.CASTLE_GEOFENCE_CENTER_LON || 28.9784);\n const cfgRadiusM = Math.max(100, Number(process.env.CASTLE_GEOFENCE_RADIUS_M || 55000));\n const cfgSpeedCap = Math.max(1, Number(process.env.CASTLE_SPEED_CAP_MPS || 35));\n const dist = Number.isFinite(lat) && Number.isFinite(lon) ? haversineMeters(cfgCenterLat, cfgCenterLon, lat, lon) : 0;\n const geofenceOk = !Number.isFinite(dist) ? true : dist <= cfgRadiusM;\n const speedOk = speed <= cfgSpeedCap;\n const failsafe = !(geofenceOk && speedOk);\n const safety = {\n geofenceOk,\n speedOk,\n failsafe,\n distanceFromCenterM: Number.isFinite(dist) ? dist : null,\n speedCapMps: cfgSpeedCap\n };\n const row = await appendTelemetry(auth.uid, deviceId, {\n lat,\n lon,\n speed,\n battery: payload?.battery,\n mode: payload?.mode || (failsafe ? \"FAILSAFE_HOLD\" : \"ACTIVE\"),\n raw: payload?.raw || {}\n });\n await updateDevice(auth.uid, deviceId, {\n status: failsafe ? \"failsafe\" : \"active\",\n lastTelemetryAt: row.ts,\n safetyState: safety\n });\n if (failsafe) {\n await addTranscript(auth.uid, {\n source: \"robotics-guardrails\",\n eventType: \"failsafe\",\n text: `Device ${deviceId} failsafe triggered geofenceOk=${geofenceOk} speedOk=${speedOk}`,\n roomId: \"studio-main\",\n meta: { deviceId, safety }\n });\n }\n sendJson(res, 200, { ok: true, telemetry: row, safety, command: failsafe ? \"FAILSAFE_HOLD\" : \"CONTINUE\" });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"telemetry_ingest_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listTelemetry(auth.uid, 150);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createAutomation(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"automation-scheduled\",\n text: `Automation scheduled: ${row.title} at ${new Date(row.scheduleAt).toLocaleString()}`,\n roomId: \"studio-main\",\n meta: { automationId: row.id, action: row.action, deviceId: row.deviceId }\n });\n sendJson(res, 200, { ok: true, automation: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"automation_create_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAutomations(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listDevices(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const devs = await listDevices(auth.uid, 500);\n const d = devs.find((x) => x.id === deviceId);\n if (!d) return sendJson(res, 404, { ok: false, error: \"device_not_found\" });\n const action = String(payload?.action || \"\");\n const caps = Array.isArray(d.capabilityProfile) ? d.capabilityProfile : [];\n if (action && caps.length && !caps.includes(action) && !caps.includes(\"*\")) {\n return sendJson(res, 400, { ok: false, error: \"unsupported_action_for_device\", capabilityProfile: caps });\n }\n const cmd = await enqueueCommand(auth.uid, {\n deviceId,\n action: action || \"NOOP\",\n params: payload?.params || {},\n adapter: payload?.adapter || d.adapter || \"websocket\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-command-queue\",\n eventType: \"command-queued\",\n text: `${cmd.deviceId} -> ${cmd.action}`,\n roomId: \"studio-main\",\n meta: { commandId: cmd.id, adapter: cmd.adapter }\n });\n sendJson(res, 200, { ok: true, command: cmd });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listCommands(auth.uid, 200);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/robotics/commands/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updateCommandStatus(auth.uid, id, payload?.status || \"sent\");\n sendJson(res, 200, { ok: true, command: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/rhizoh/bridge\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const result = await runRhizohBrainV2({\n message: String(payload?.message || \"robotics bridge command\"),\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n worldState: payload?.worldState || {},\n deviceState: payload?.deviceState || {},\n intent: \"robotics-mechanics\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-bridge\",\n eventType: \"robotics-command\",\n text: `Bridge -> ${String(result.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { directive: result.directive, agentId: payload?.agentId || \"\" }\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"robotics_bridge_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/planner/graph\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const graph = buildTaskActionGraph(String(payload?.task || \"\"), payload?.context || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"planner-graph\",\n text: `Planner graph built: ${graph.taskTitle} nodes=${graph.nodes.length}`,\n roomId: \"studio-main\",\n meta: { taskTitle: graph.taskTitle, nodeCount: graph.nodes.length }\n });\n sendJson(res, 200, { ok: true, graph });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"planner_graph_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerSocialChannel(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, channel: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_channel_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listSocialChannels(auth.uid, 100);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/broadcast\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const text = String(payload?.text || \"\").slice(0, 1200);\n const channels = await listSocialChannels(auth.uid, 100);\n const enabled = channels.filter((c) => c.status === \"enabled\");\n const deliveries = [];\n for (const c of enabled) {\n const out = await deliverSocialWithRetry(c, text);\n deliveries.push({ id: c.id, platform: c.platform, ...out });\n }\n const okCount = deliveries.filter((d) => d.ok).length;\n await addTranscript(auth.uid, {\n source: \"social-layer\",\n eventType: \"social-broadcast\",\n text: `Broadcast delivered ${okCount}/${enabled.length} channel(s): ${text.slice(0, 240)}`,\n roomId: \"studio-main\",\n meta: { deliveries }\n });\n sendJson(res, 200, { ok: true, sentChannels: okCount, totalChannels: enabled.length, deliveries });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_broadcast_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/ethics/programs\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, ...buildEthicsPrograms() });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands/dispatch\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const all = await listCommands(auth.uid, 200);\n const queued = all.filter((c) => String(c.status || \"queued\") === \"queued\").slice(0, 50);\n const devices = await listDevices(auth.uid, 500);\n const byId = new Map(devices.map((d) => [d.id, d]));\n const results = [];\n for (const cmd of queued) {\n const dev = byId.get(cmd.deviceId);\n if (!dev) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: \"device_not_found\" });\n continue;\n }\n try {\n const dispatched = await dispatchCommandToAdapter(dev, cmd);\n await updateCommandStatus(auth.uid, cmd.id, \"sent\");\n results.push({ id: cmd.id, ok: true, adapter: dispatched.adapter, via: dispatched.via });\n } catch (error) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: String(error?.message || \"dispatch_failed\") });\n }\n }\n await addTranscript(auth.uid, {\n source: \"robotics-command-dispatch\",\n eventType: \"command-dispatch-batch\",\n text: `Dispatch batch size=${results.length} ok=${results.filter((x) => x.ok).length}`,\n roomId: \"studio-main\",\n meta: { results: results.slice(0, 30) }\n });\n sendJson(res, 200, { ok: true, processed: results.length, results });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"dispatch_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/pdf/jobs/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n const job = await getPdfJob(auth.uid, id);\n if (!job) return sendJson(res, 404, { ok: false, error: \"pdf_job_not_found\" });\n sendJson(res, 200, { ok: true, job });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const row = await appendMemory({\n scope,\n id,\n text: payload?.text,\n tags: payload?.tags || [],\n importance: payload?.importance,\n kind: payload?.kind || \"episodic\",\n meta: { uid: auth.uid, ...(payload?.meta || {}) }\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.externalTruth) {\n try {\n if (process.env.RHIZOH_EXTERNAL_TRUTH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_truth:${ip}`, RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n sendJson(res, 200, buildRhizohExternalGroundTruthPayload(req));\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.productOutcome) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const rawBody = await readHttpBodyText(req, 64 * 1024);\n const body = rawBody.trim() ? safeJsonParse(rawBody) : null;\n if (!body || typeof body !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"invalid_json_payload\" });\n return;\n }\n const sigHdr = String(req.headers?.[\"x-rhizoh-outcome-signature\"] || \"\").trim();\n const hmacV = verifyRhizohOutcomeHmac(rawBody, sigHdr);\n if (!hmacV.ok) {\n sendJson(res, 401, { ok: false, error: hmacV.error || \"outcome_hmac_invalid\" });\n return;\n }\n const srcTokHdr = String(req.headers?.[\"x-rhizoh-outcome-source-token\"] || \"\").trim();\n const srcV = verifyRhizohOutcomeSourceToken(body.source, srcTokHdr);\n if (!srcV.ok) {\n sendJson(res, 401, { ok: false, error: srcV.error || \"outcome_source_token_invalid\" });\n return;\n }\n const subjectKey = String(body.subjectKey || \"\").trim().slice(0, 128);\n if (\n subjectKey &&\n !checkHttpRateLimit(\n `rhizoh_product_outcome_subj:${subjectKey}`,\n RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN,\n 60_000\n )\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_subject\" });\n return;\n }\n const result = ingestRhizohProductOutcomeHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n schemaVersion: \"1.1.0\",\n aggregate: result.aggregate,\n integrity: result.integrity\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"outcome_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.productOutcomeAggregate) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome_agg:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const u = new URL(String(req.url || \"/\"), \"http://localhost\");\n const cohortId = u.searchParams.get(\"cohortId\") || \"\";\n const decisionFingerprint = u.searchParams.get(\"decisionFingerprint\") || \"\";\n const limit = u.searchParams.get(\"limit\") || \"50\";\n const rows = listRhizohProductOutcomeAggregates({\n cohortId: cohortId || undefined,\n decisionFingerprint: decisionFingerprint || undefined,\n limit: Number(limit) || 50\n });\n sendJson(res, 200, { ok: true, schemaVersion: \"1.1.0\", rows });\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.externalLossBatch) {\n try {\n if (process.env.RHIZOH_EXTERNAL_LOSS_BATCH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_loss_batch:${ip}`, RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const result = ingestRhizohExternalLossBatchHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, { ok: true, accepted: result.accepted ?? 0 });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"batch_failed\") });\n }\n return;\n }\n\n function moderationAdminOk(req) {\n const expected = String(process.env.CASTLE_MODERATION_ADMIN_KEY || \"\").trim();\n if (!expected) return false;\n const got = String(req.headers[\"x-castle-moderation-key\"] || \"\").trim();\n return got.length > 0 && got === expected;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.opsAbuseReport) {\n try {\n const body = await readHttpJson(req, 32 * 1024);\n const auth = await resolveHttpUser(req);\n const entry = submitAbuseReportV0({\n reporterUid: auth.ok ? auth.uid : null,\n targetUid: body?.targetUid ?? null,\n category: body?.category,\n detail: body?.detail,\n traceId: body?.traceId ?? null\n });\n sendJson(res, 202, { ok: true, reportId: entry.id, status: entry.status });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsModerationQueue) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n queue: listModerationQueueV0(100),\n softBlocked: listSoftBlockedUsersV0()\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-block\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const out = softBlockUserV0(body?.uid, body?.reason);\n sendJson(res, out.ok ? 200 : 400, out);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-unblock\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n sendJson(res, 200, removeSoftBlockV0(body?.uid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsAgentSnapshots) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n const limit = Number(new URL(req.url || \"\", \"http://local\").searchParams.get(\"limit\") || 50);\n sendJson(res, 200, { ok: true, snapshots: listRecentAgentSnapshotsV0(limit) });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.academicObservatoryExport) {\n const auth = await resolveHttpUser(req);\n const out = handleAcademicObservatoryExportGetV0(req, auth);\n sendJson(res, out.status, out.body);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsHardeningStatus) {\n const auth = await resolveHttpUser(req);\n const principal = auth.ok ? `uid:${auth.uid}` : null;\n const gclSnap = await getGlobalCostLedgerSnapshotV0(principal);\n const economicStrategy = evaluateEconomicStrategyL3V0(gclSnap);\n const hardeningTenantId = resolveHardeningTenantIdV0(auth, req);\n const unifiedState = await buildUnifiedStateNarrativeV0({\n tenantId: hardeningTenantId ?? undefined,\n platformScope: !hardeningTenantId,\n principal\n });\n sendJson(res, 200, {\n ok: true,\n schema: \"rhizoh.ops.hardening_status.v0\",\n containment: readAgentContainmentConfigV0(),\n cost: readCostContainmentConfigV0(),\n gcl: {\n backend: await getGclBackendStatusV0(),\n snapshot: gclSnap\n },\n economicStrategyL3: economicStrategy,\n phasedRollout: await getPhasedRolloutStatsV0(),\n tier: readPhasedRolloutTierV0(),\n principalCost: auth.ok ? await getCostContainmentStatsV0(principal) : null,\n stressTaxonomy: verifyStressTaxonomyCoverageV0(),\n stressConflictResolution: verifyStressConflictResolutionV0(),\n resolutionStabilityEnvelope: verifyResolutionStabilityEnvelopeV0(),\n unifiedState: {\n tenantScope: {\n tenantId: unifiedState.tenantScope?.tenantId,\n scope: unifiedState.tenantScope?.scope,\n noGlobalDerivedState: unifiedState.tenantScope?.noGlobalDerivedState\n },\n narrativeFingerprint: unifiedState.narrativeFingerprint,\n screenshotScopeWatermark: unifiedState.screenshotScopeWatermark,\n interpretationSafetyContract: unifiedState.interpretationSafetyContract,\n interpretationUxContract: {\n schema: unifiedState.interpretationUxContract?.schema,\n layerOrder: unifiedState.interpretationUxContract?.layerOrder,\n narrativeNeverVisuallyDominant: unifiedState.interpretationUxContract?.narrativeNeverVisuallyDominant\n },\n governance: {\n narrativeIsNotDecisionLayer: unifiedState.governance.narrativeIsNotDecisionLayer,\n displayOrder: unifiedState.governance.displayOrder,\n trustPosture: unifiedState.governance.trustPosture,\n interpretationUxContract: unifiedState.governance.interpretationUxContract?.schema\n },\n humanOps: {\n decisionTier: unifiedState.humanOps?.humanDecisionScaling?.tier,\n decisionOwner: unifiedState.humanOps?.humanDecisionScaling?.decisionOwner,\n opsRouteId: unifiedState.humanOps?.humanDecisionOpsRunbook?.routing?.routeId,\n slaAckMinutes: unifiedState.humanOps?.humanDecisionOpsRunbook?.sla?.ackMinutes,\n uiDisclaimerRequired: unifiedState.humanOps?.narrativeUiSafety?.displayRules?.requireDisclaimerBanner,\n misreadHighResidual: unifiedState.humanOps?.misreadSimulation?.highResidualCount,\n propagationHighResidual: unifiedState.humanOps?.socialPropagationSimulation?.highResidualCount,\n worstWatermarkSurvivability:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.worstWatermarkSurvivability,\n dominantDistortionSource:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.dominantDistortionSource\n },\n culturalRisk: {\n perceptualRiskClass: unifiedState.culturalRisk?.perceptualRiskClass,\n trustFork: unifiedState.culturalRisk?.trustDynamics?.fork,\n trustDecayScore: unifiedState.culturalRisk?.trustDynamics?.scores?.trustDecay,\n mythologyScore: unifiedState.culturalRisk?.trustDynamics?.scores?.mythology,\n confidenceDetachmentExposure:\n unifiedState.culturalRisk?.trustDynamics?.axes?.confidenceDetachmentExposure\n },\n appliedSystemsLayer: {\n schema: unifiedState.appliedSystemsLayer?.schema,\n researchOutputType: unifiedState.appliedSystemsLayer?.researchGovernance?.outputType,\n roboticsBlockActuation: unifiedState.appliedSystemsLayer?.roboticsGrounding?.blockActuation,\n spiralRumorCount:\n unifiedState.appliedSystemsLayer?.spiralMMOGameKernel?.gameLayer?.rumors?.length\n },\n actionSemanticGovernance: {\n schema: unifiedState.actionSemanticGovernance?.schema,\n highRiskActionCount: unifiedState.actionSemanticGovernance?.highRiskActionCount,\n loopClosed: unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.loopClosed,\n authorityCollapseMitigated:\n unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.risks?.authorityCollapse\n ?.mitigated\n },\n actionContextResolution: {\n schema: unifiedState.actionContextResolution?.schema,\n contextFingerprint: unifiedState.actionContextResolution?.contextFingerprint?.fingerprintHash,\n domainCollision: unifiedState.actionContextResolution?.domainCollision?.detected,\n deployAgentSpiralBlocked:\n unifiedState.actionContextResolution?.actionOverloadingRisk?.deployAgentExample\n ?.spiral_mmo === \"blocked\",\n roboticsActuationEligible:\n unifiedState.actionContextResolution?.roboticsActuationCorrectness?.actuationEligible\n },\n epistemicCoherence: {\n schema: unifiedState.epistemicCoherence?.schema,\n verdict: unifiedState.epistemicCoherence?.systemCoherence?.verdict,\n coherenceScore: unifiedState.epistemicCoherence?.systemCoherence?.score,\n contradictionCount: unifiedState.epistemicCoherence?.crossLayerContradictions?.count,\n operatorDecision: unifiedState.epistemicCoherence?.uxCompression?.operatorDecision,\n headlineTr: unifiedState.epistemicCoherence?.uxCompression?.headline?.tr,\n falseSafetyWarning: Boolean(\n unifiedState.epistemicCoherence?.uxCompression?.falseSafetyWarning\n )\n },\n coherenceAuthorityBoundary: {\n schema: unifiedState.coherenceAuthorityBoundary?.schema,\n bindingValid: unifiedState.coherenceAuthorityBoundary?.eclAuthorityBinding?.valid,\n canExecute: unifiedState.coherenceAuthorityBoundary?.contract?.can_execute,\n mandatoryBannerTr:\n unifiedState.coherenceAuthorityBoundary?.disclaimers?.mandatoryBanner?.tr\n },\n decisionLatencyGovernance: {\n schema: unifiedState.decisionLatencyGovernance?.schema,\n latencyTier: unifiedState.decisionLatencyGovernance?.latencyTier,\n inflationScore: unifiedState.decisionLatencyGovernance?.latencyInflation?.score,\n fastPathEligible: unifiedState.decisionLatencyGovernance?.fastPath?.eligible,\n primaryActionId: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.primaryActionId,\n ackSlaMinutes: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.ackSlaMinutes,\n certaintyCap: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.certaintyCap,\n uncertaintyItems:\n unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.uncertaintyEnvelope?.itemCount\n },\n decisionPacketUncertaintyBoundary: {\n schema: unifiedState.decisionPacketUncertaintyBoundary?.schema,\n invariantValid: unifiedState.decisionPacketUncertaintyBoundary?.invariantCheck?.valid,\n centralInvariant: unifiedState.decisionPacketUncertaintyBoundary?.centralInvariant\n },\n epistemicDecisionPacing: {\n schema: unifiedState.epistemicDecisionPacing?.schema,\n queueSaturationTier:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation?.tier,\n operatorProcessingLatency:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation\n ?.operatorProcessingLatency,\n postureWindowCount:\n unifiedState.epistemicDecisionPacing?.temporaryStaticPostureWindows\n ?.temporaryStaticPostureWindows?.windowCount,\n executionBoundariesValid:\n unifiedState.epistemicDecisionPacing?.executionBoundaries?.valid,\n grantsExecutionApproval: unifiedState.epistemicDecisionPacing?.grantsExecutionApproval\n },\n epistemicTemporalCoherence: {\n schema: unifiedState.epistemicTemporalCoherence?.schema,\n allInvariantsValid: unifiedState.epistemicTemporalCoherence?.allInvariantsValid,\n temporalAlignmentValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.temporalAlignment?.valid,\n crossWindowGuardValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard?.valid,\n delayedTruthValid: unifiedState.epistemicTemporalCoherence?.invariants?.delayedTruthRule?.valid,\n reconciliationRequired:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard\n ?.reconciliation?.required\n },\n realityDriftObserver: {\n schema: unifiedState.realityDriftObserver?.schema,\n driftActive: unifiedState.realityDriftObserver?.driftObserverSummary?.active,\n misapprehensionShapeCount:\n unifiedState.realityDriftObserver?.misapprehensionShapeCatalog?.shapeCount,\n criticalShapeCount:\n unifiedState.realityDriftObserver?.driftObserverSummary?.criticalShapeCount,\n roboticsMismatchActive:\n unifiedState.realityDriftObserver?.roboticsFeedbackMismatch?.active\n },\n driftCausality: {\n schema: unifiedState.driftCausality?.schema,\n shapesWithCausalExplanation: unifiedState.driftCausality?.rdolLinkage?.shapesWithCausalExplanation,\n activeDomainCount: unifiedState.driftCausality?.causalitySummary?.activeDomainCount,\n propagationCausalityActive: unifiedState.driftCausality?.causalDomains?.propagation?.active\n },\n productReadiness: unifiedState.productReadiness,\n stateLayers: {\n raw: unifiedState.stateLayers.raw,\n derived: {\n systemState: unifiedState.stateLayers.derived.systemState,\n narrative: unifiedState.stateLayers.derived.narrative,\n validation: unifiedState.stateLayers.derived.validation\n },\n policy: {\n binding: unifiedState.stateLayers.policy.binding,\n decisionOwner: unifiedState.stateLayers.policy.decisionOwner,\n prohibitedActions: unifiedState.stateLayers.policy.prohibitedActions\n }\n },\n systemState: unifiedState.systemState,\n interpretation: unifiedState.interpretation,\n validation: {\n trustPosture: unifiedState.validation.trustPosture,\n adjustedConfidence: unifiedState.validation.adjustedConfidence,\n agreement: unifiedState.validation.confidenceDecomposition.agreement,\n divergenceScore: unifiedState.validation.divergence.divergenceScore,\n validated: unifiedState.validation.divergence.validated,\n uncertaintyTags: unifiedState.validation.uncertainty.tags.map((t) => t.id),\n flags: unifiedState.validation.divergence.flags.map((f) => f.id)\n },\n compression: unifiedState.compression\n }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 6 * 1024 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (\n !checkHttpRateLimit(`rhizoh_voice_transcribe:${rlKey}`, RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN, 60_000)\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const voiceEnv = rhizohVoiceTranscribeEnvV3();\n if (!voiceEnv.any) {\n sendJson(res, 503, {\n ok: false,\n error: \"voice_asr_not_configured\",\n env: voiceEnv\n });\n return;\n }\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n const result = await runRhizohVoiceTranscribeV3(\n applyLanguagePropagationToVoiceBodyV1(payload, langProp)\n );\n sendJson(res, result.ok ? 200 : 422, {\n ...result,\n traceId: String(payload?.traceId || \"\"),\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n atMs: Date.now()\n });\n } catch (error) {\n sendJson(res, 500, {\n ok: false,\n error: \"voice_transcribe_failed\",\n detail: String(error?.message || error),\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(\"/rhizoh/llm/task/\")) {\n try {\n const rawTaskId = decodeURIComponent(pathname.slice(\"/rhizoh/llm/task/\".length).split(\"/\")[0] || \"\");\n const taskId = String(rawTaskId || \"\").toLowerCase() === \"last\" ? getLatestLlmWorkerTaskIdV0() : rawTaskId;\n const snap = taskId ? getLlmWorkerTaskSnapshotV0(taskId) : null;\n if (!snap) {\n sendJson(res, 404, {\n ok: false,\n error: \"llm_task_not_found\",\n taskId: rawTaskId,\n status: \"missing\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n if (snap.status === \"processing\") {\n sendJson(res, 202, {\n ok: true,\n taskId: snap.taskId,\n status: \"processing\",\n createdAt: snap.createdAt,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n // Terminal states (completed/failed) always HTTP 200 — poll must not look like server fault (500).\n sendJson(res, 200, {\n ...(snap.body || { ok: false, error: \"llm_task_empty\", taskId: snap.taskId }),\n taskId: snap.taskId,\n status: snap.status === \"completed\" ? \"completed\" : snap.status,\n terminal: true,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n } catch (error) {\n console.error(\"[llm-task-poll] handler failed:\", error);\n sendJson(res, 200, {\n ok: false,\n error: \"llm_task_poll_handler_failed\",\n detail: String(error?.message || error),\n status: \"failed\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === rhizohRuntime.routes.rhizohLlm) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 128 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n logRhizohHealth(\"gateway_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: String(payload?.traceId || \"\"),\n auth: auth.ok ? \"ok\" : \"anon\",\n ip\n });\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (!checkHttpRateLimit(`rhizoh_llm:${rlKey}`, RL_RHIZOH_LLM_PER_MIN, 60_000)) {\n const rlTaxonomy = classifyStressResponseV0({ code: \"rate_limit_exceeded\" });\n return sendJson(res, 429, {\n ok: false,\n error: \"rate_limit_exceeded\",\n reply: rlTaxonomy.userMessageTr || \"İstek sınırı aşıldı. Kısa süre sonra tekrar deneyin.\",\n stressClass: rlTaxonomy.stressClass,\n responseAction: rlTaxonomy.responseAction,\n stressMatrix: rlTaxonomy.matrix,\n stressInterpretable: rlTaxonomy.interpretable,\n stressConfidence: rlTaxonomy.stressConfidence,\n actionConfidence: rlTaxonomy.actionConfidence,\n actionSoftened: rlTaxonomy.actionSoftened,\n responseActionStrict: rlTaxonomy.responseActionStrict,\n actionInterpretable: rlTaxonomy.actionInterpretable,\n conflictResolution: rlTaxonomy.conflictResolution,\n directive: \"NONE\"\n });\n }\n\n const requireExplicit = process.env.CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE === \"1\";\n const keyMode = normalizeClientLlmKeySource(payload?.llmKeySource ?? payload?.keySource);\n if (keyMode === null) {\n return sendJson(res, 400, {\n ok: false,\n error: \"invalid_llm_key_source\",\n reply: \"llmKeySource geçersiz. Kullanın: env | user_connection | auto\",\n directive: \"NONE\"\n });\n }\n if (requireExplicit && keyMode === \"auto\") {\n return sendJson(res, 400, {\n ok: false,\n error: \"llm_key_source_required\",\n reply: \"Sunucu CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE=1: istekte llmKeySource (env veya user_connection) zorunlu.\",\n directive: \"NONE\"\n });\n }\n\n const conn = auth.ok ? await resolveConnection(auth.uid, String(payload?.connectionId || \"\")) : null;\n\n if (keyMode === \"user_connection\") {\n if (!auth.ok) {\n return sendJson(res, 401, {\n ok: false,\n error: \"llm_auth_required_for_user_connection\",\n reply: \"Kayıtlı LLM bağlantısı için giriş gerekli.\",\n directive: \"NONE\"\n });\n }\n if (!conn?.apiKey) {\n return sendJson(res, 400, {\n ok: false,\n error: \"user_llm_connection_required\",\n reply: \"Geçerli bir LLM bağlantısı yok; Studio’da bağlantı ekleyin veya llmKeySource=env kullanın.\",\n directive: \"NONE\"\n });\n }\n }\n\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n let safePayload = applyLanguagePropagationToLlmPayloadV1({ ...(payload || {}) }, langProp);\n const trimOut = trimRhizohLlmGatewayPayloadV1(safePayload);\n if (trimOut.meta.trimmed) {\n logRhizohHealth(\"llm_payload_trimmed\", {\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n bytesBefore: trimOut.meta.bytesBefore,\n bytesAfter: trimOut.meta.bytesAfter\n });\n }\n safePayload = trimOut.payload;\n delete safePayload.apiKey;\n delete safePayload.llmKeySource;\n delete safePayload.keySource;\n\n const llmMessage = String(\n safePayload?.message || safePayload?.text || payload?.message || payload?.text || \"\"\n ).trim();\n if (!llmMessage) {\n return sendJson(res, 400, {\n ok: false,\n error: \"message_required\",\n reply: \"Mesaj metni gerekli (message alanı boş).\",\n directive: \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n if (!safePayload.message) {\n safePayload = { ...safePayload, message: llmMessage };\n }\n\n let resolvedProvider = payload?.provider;\n let resolvedModel = payload?.model;\n let connApiKey = \"\";\n\n ({\n resolvedProvider,\n resolvedModel,\n connApiKey\n } = resolveServerLlmProviderV0(keyMode, conn, payload));\n\n const turnInput = {\n safePayload,\n auth,\n keyMode,\n conn,\n resolvedProvider,\n resolvedModel,\n connApiKey\n };\n const turnMeta = {\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n langEcho: buildRhizohLanguagePropagationEchoV1(langProp)\n };\n\n if (\n isLlmWorkerEnabledV0() &&\n isLlmWorkerAsyncEnabledV0() &&\n String(req.headers[\"x-rhizoh-llm-sync\"] || \"\").trim() !== \"1\"\n ) {\n const taskId = enqueueLlmWorkerTaskAsyncV0(turnInput, turnMeta);\n logRhizohHealth(\"llm_worker_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n taskId,\n traceId: String(payload?.traceId || taskId),\n keyMode,\n async: true\n });\n sendJson(res, 202, {\n ok: true,\n taskId,\n status: \"processing\",\n traceId: String(payload?.traceId || taskId),\n pollPath: `/rhizoh/llm/task/${taskId}`,\n gatewayPid: process.pid,\n ...turnMeta.langEcho\n });\n return;\n }\n\n const { result, traceId, turnLatencyMs, spinePhases, sampledTrace } = isLlmWorkerEnabledV0()\n ? await runLlmWorkerTaskV0(turnInput, turnMeta)\n : await rhizohGatewayTurn(turnInput);\n logRhizohHealth(\"llm_response\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId,\n turnLatencyMs: Number(turnLatencyMs || 0),\n keyMode\n });\n sendJson(res, 200, {\n ...result,\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n traceId,\n turnLatencyMs,\n ...(spinePhases ? { spinePhases } : {}),\n ...(sampledTrace !== undefined ? { sampledTrace } : {})\n });\n } catch (error) {\n const msg = String(error?.message || \"\");\n const code = error?.code || \"\";\n let status = 500;\n if (msg === \"payload_too_large\") status = 413;\n else if (msg === \"rate_limit_exceeded\") status = 429;\n else if (\n code === \"cost_hard_limit\" ||\n code === \"phased_rollout_capacity\" ||\n error?.containment === true ||\n String(code).startsWith(\"agent_\")\n ) {\n status = 429;\n } else if (code === \"user_soft_blocked\" || code === \"prompt_abuse_detected\") status = 403;\n else if (\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(code) ||\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(msg) ||\n msg.includes(\"missing_api_key_for_\")\n )\n status = 400;\n let rhizohFailureKind = \"provider_error\";\n if (msg === \"rate_limit_exceeded\" || status === 429) rhizohFailureKind = \"rate_limit\";\n else if (status === 403) rhizohFailureKind = \"policy_block\";\n else if (status === 400 || status === 413) rhizohFailureKind = \"client_config\";\n else if (msg.startsWith(\"provider_http_\")) {\n const st = Number(msg.replace(\"provider_http_\", \"\"));\n if (st === 408 || st === 504) rhizohFailureKind = \"timeout\";\n else rhizohFailureKind = \"provider_error\";\n } else if (/timeout|timed out|ETIMEDOUT/i.test(msg)) rhizohFailureKind = \"timeout\";\n\n const providerHttpMatch = msg.match(/^provider_http_(\\d{3})$/);\n const stressTaxonomy =\n error?.stressClass != null\n ? {\n stressClass: error.stressClass,\n responseAction: error.responseAction,\n stressMatrix: error.stressMatrix,\n stressInterpretable: error.stressInterpretable !== false,\n stressConfidence: error.stressConfidence,\n actionConfidence: error.actionConfidence,\n actionSoftened: error.actionSoftened,\n responseActionStrict: error.responseActionStrict,\n actionInterpretable: error.actionInterpretable,\n conflictResolution: error.conflictResolution,\n stressSecondary: error.stressSecondary,\n responseActions: error.responseActions\n }\n : (() => {\n const t = classifyStressResponseV0({\n code: code || msg,\n rhizohFailureKind,\n providerHttpStatus: providerHttpMatch\n ? Number(providerHttpMatch[1])\n : null\n });\n return {\n stressClass: t.stressClass,\n responseAction: t.responseAction,\n stressMatrix: t.matrix,\n stressInterpretable: t.interpretable,\n stressConfidence: t.stressConfidence,\n actionConfidence: t.actionConfidence,\n actionSoftened: t.actionSoftened,\n responseActionStrict: t.responseActionStrict,\n actionInterpretable: t.actionInterpretable,\n conflictResolution: t.conflictResolution,\n stressSecondary: t.stressSecondary,\n responseActions: t.responseActions\n };\n })();\n sendJson(res, status, {\n ok: false,\n rhizohFailureKind,\n ...(providerHttpMatch ? { providerHttpStatus: Number(providerHttpMatch[1]) } : {}),\n error: code || msg || \"rhizoh_llm_failed\",\n detail: msg || code || undefined,\n reply:\n error?.reply ||\n (status === 500 ? \"Rhizoh bağlantısı geçici olarak kesildi.\" : msg),\n directive: error?.directive || \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n ...stressTaxonomy\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicSeal) {\n try {\n if (!EPISTEMIC_SEAL_SECRET) {\n sendJson(res, 503, { ok: false, error: \"epistemic_seal_disabled\" });\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`epistemic_seal:${ip}`, RL_EPISTEMIC_SEAL_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const truth = body.truth_contract;\n if (!truth || typeof truth !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"truth_contract_required\" });\n return;\n }\n const sealBody = {\n truth_contract: truth,\n runtime_hash: String(body.runtime_hash || \"\"),\n model_route: {\n provider: body.model_route?.provider ?? null,\n model: body.model_route?.model ?? null\n },\n memory_digest: String(body.memory_digest || \"\"),\n world_snapshot_hash: String(body.world_snapshot_hash || \"\"),\n timestamp: Number(body.timestamp) || Date.now()\n };\n const canonical = canonicalEpistemicSealString(sealBody);\n const { hash, signature } = hashAndSignEpistemicSeal(canonical, EPISTEMIC_SEAL_SECRET);\n recordGenesisEpistemicSealIssued(hash);\n const attestation = {\n schema: EPISTEMIC_SEAL_SCHEMA,\n algorithm: \"SHA-256+HMAC-SHA256\",\n keyHint: process.env.CASTLE_EPISTEMIC_SEAL_SECRET ? \"CASTLE_EPISTEMIC_SEAL_SECRET\" : \"CASTLE_GATEWAY_TOKEN\",\n issuedAt: Date.now(),\n gateway: { service: \"castle-gateway\", listenPort: PORT }\n };\n sendJson(res, 200, {\n ok: true,\n hash,\n signature,\n attestation,\n canonicalBytes: Buffer.byteLength(canonical, \"utf8\")\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"seal_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicLogsBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`epistemic_logs:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 120) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const persisted = await persistEpistemicLedgerBatch(auth.uid, entries);\n recordGenesisEpistemicLedgerPersisted(persisted.persisted);\n // Internal cognition layer: forecast stays gateway/ledger-only (no client exposure).\n try {\n await persistEpistemicForecastBatch(auth.uid, persisted.normalized || []);\n } catch {\n /* forecast failures must not break primary ledger ingest */\n }\n sendJson(res, 200, {\n ok: true,\n persisted: persisted.persisted,\n mode: persisted.mode,\n latest: persisted.normalized?.[persisted.normalized.length - 1] || null\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"epistemic_log_ingest_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.authorityLedgerBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`authority_ledger:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 80) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const witnessed = persistAuthorityLedgerWitnessBatchV1(auth.uid, entries, EPISTEMIC_SEAL_SECRET);\n if (witnessed.witnessed > 0) {\n recordGenesisEpistemicLedgerPersisted(witnessed.witnessed);\n const clientSeal = witnessed.lastWitness?.clientSealHash;\n if (clientSeal) recordGenesisEpistemicSealIssued(clientSeal);\n }\n sendJson(res, 200, {\n ok: true,\n witnessed: witnessed.witnessed,\n quarantined: witnessed.quarantined,\n chainHeight: witnessed.chainHeight,\n chainHead: witnessed.chainHead,\n lastWitness: witnessed.lastWitness,\n results: witnessed.results\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"authority_ledger_witness_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.authorityLedgerReplay) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const replayUrl = new URL(String(req.url || \"\"), \"http://localhost\");\n const epochId = String(replayUrl.searchParams.get(\"epochId\") || \"\").trim() || undefined;\n const snapshot = getAuthorityLedgerWitnessSnapshotV1(auth.uid, epochId);\n const replay = replayAuthorityLedgerWitnessChainV1(auth.uid, epochId);\n sendJson(res, 200, {\n ok: true,\n snapshot,\n replay,\n schema: \"castle.rhizoh.authority_ledger_witness.v1.replay_surface\"\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"authority_ledger_replay_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.authorityEpochMerge) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`authority_epoch_merge:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const merged = assimilateAuthorityEpochMergeV1(auth.uid, body);\n if (!merged.ok) {\n sendJson(res, 400, merged);\n return;\n }\n sendJson(res, 200, {\n ok: true,\n mergeEvent: merged.mergeEvent,\n recentMerges: merged.recentMerges\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"authority_epoch_merge_failed\") });\n }\n return;\n }\n\n sendJson(res, 404, { ok: false, error: \"not_found\" });\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\nspiralWssForGenesis = wss;\nconst orchestrator = createOrchestrator();\nconst clientStats = new Map();\nlet broadcasterClientId = null;\nconst spiralState = {\n rooms: [{ id: \"room-default\", name: \"Spiral Default Arena\" }],\n activeByClient: new Map(),\n characters: new Map() // id -> {id,name,role,roomId,source}\n};\n\n/** Castle social rooms — keyed by `castleRoomKey` from {@link WS_MESSAGE.CASTLE_SOCIAL_PULSE}. */\nconst castleSocialRoomState = new Map();\nconst CASTLE_SOCIAL_MEMBER_TTL_MS = 90_000;\nconst CASTLE_SOCIAL_MAX_PULSE_BYTES = 8192;\nconst RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES = 5 * 1024 * 1024;\nconst RHIZOH_VOICE_LIVE_MAX_CHUNKS = 96;\n\n/**\n * Voice Engine v3 live media lane.\n * Client streams MediaRecorder chunks over the existing gateway WS; STOP reuses the\n * gateway ASR runner so orchestration remains the single authority downstream.\n */\nconst rhizohVoiceLiveSessions = new WeakMap();\n\nfunction appendRhizohVoiceLiveEventV0(session, type, payloadRefSeed = \"\") {\n if (!session) return null;\n session.eventSeq = (Number(session.eventSeq) || 0) + 1;\n const event = createRhizohImmutableEventV0({\n type,\n sessionId: session.sessionId,\n traceId: session.traceId,\n actorSeed: session.sessionId || session.traceId,\n eventSeq: session.eventSeq,\n payloadRef: createRhizohPayloadRefV0(payloadRefSeed || `${session.sessionId}:${type}:${session.eventSeq}`)\n });\n session.events = [...(session.events || []), event].slice(-64);\n return event;\n}\n\nfunction sendRhizohVoiceLiveEnvelopeV0(socket, type, payload) {\n try {\n if (socket?.readyState === 1) socket.send(JSON.stringify(createEnvelope(type, payload)));\n } catch {\n /* noop */\n }\n}\n\nfunction decodeRhizohVoiceLiveChunkV0(payload = {}) {\n const b64 = String(payload.audioBase64 || \"\").trim();\n if (!b64) return { ok: false, error: \"audio_base64_required\" };\n try {\n const buf = Buffer.from(b64, \"base64\");\n if (!buf.length) return { ok: false, error: \"audio_decode_empty\" };\n return { ok: true, buffer: buf };\n } catch {\n return { ok: false, error: \"audio_base64_invalid\" };\n }\n}\n\nfunction handleRhizohVoiceLiveStartV0(socket, payload = {}) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const ctx = resolveVoiceWorldContextV0(p);\n const sessionId = String(ctx.sessionId).slice(0, 96);\n const session = {\n sessionId,\n worldId: ctx.worldId,\n boundMatchSessionId: ctx.boundMatchSessionId,\n traceId: String(p.traceId || \"\").slice(0, 128),\n mimeType: String(p.mimeType || \"audio/webm\").slice(0, 80),\n languageCode: String(p.languageCode || \"tr-TR\").slice(0, 32),\n path: String(p.path || \"both\").slice(0, 24),\n chunks: [],\n chunkIndexes: [],\n totalBytes: 0,\n nextChunkIndex: 1,\n eventSeq: 0,\n events: [],\n startedAtMs: Date.now()\n };\n rhizohVoiceLiveSessions.set(socket, session);\n registerVoiceGatewayCitizenV0(socket, {\n sessionId,\n worldId: ctx.worldId,\n boundMatchSessionId: ctx.boundMatchSessionId,\n traceId: session.traceId\n });\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SESSION_STARTED\", `${sessionId}:live_session`);\n sendRhizohVoiceLiveEnvelopeV0(\n socket,\n WS_MESSAGE.RHIZOH_VOICE_LIVE_START,\n enrichVoiceLivePayloadV0(session, \"VOICE_SESSION_START\", {\n ok: true,\n sessionId,\n schema: \"castle.rhizoh.voice_live.gateway_lane.v0\"\n })\n );\n}\n\nfunction handleRhizohVoiceLiveChunkV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const sequence = validateRhizohVoiceLiveChunkPayloadV0(payload, {\n expectedChunkIndex: session.nextChunkIndex\n });\n if (!sequence.ok) {\n rhizohVoiceLiveSessions.delete(socket);\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:chunk_reject`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: sequence.error,\n expectedChunkIndex: sequence.expectedChunkIndex,\n chunkIndex: sequence.chunkIndex\n });\n return;\n }\n const decoded = decodeRhizohVoiceLiveChunkV0(payload);\n if (!decoded.ok) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: decoded.error\n });\n return;\n }\n if (\n session.chunks.length >= RHIZOH_VOICE_LIVE_MAX_CHUNKS ||\n session.totalBytes + decoded.buffer.length > RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES\n ) {\n rhizohVoiceLiveSessions.delete(socket);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_audio_too_large\"\n });\n return;\n }\n session.chunks.push(decoded.buffer);\n session.chunkIndexes.push(sequence.chunkIndex);\n session.totalBytes += decoded.buffer.length;\n session.nextChunkIndex += 1;\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_CHUNK_ACCEPTED\", `${session.sessionId}:chunk:${sequence.chunkIndex}`);\n}\n\nfunction handleRhizohVoiceLiveStopV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n rhizohVoiceLiveSessions.delete(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const lastChunkIndex = Number(payload?.lastChunkIndex);\n if (Number.isInteger(lastChunkIndex) && lastChunkIndex !== session.chunks.length) {\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:stop_mismatch`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_chunk_count_mismatch\",\n expectedChunkCount: lastChunkIndex,\n receivedChunkCount: session.chunks.length\n });\n return;\n }\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_STOP_RECEIVED\", `${session.sessionId}:stop:${session.chunks.length}`);\n const audio = Buffer.concat(session.chunks);\n if (!audio.length) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"audio_empty\"\n });\n return;\n }\n runRhizohVoiceTranscribeV3({\n path: String(payload?.path || session.path || \"both\"),\n audioBase64: audio.toString(\"base64\"),\n mimeType: String(payload?.mimeType || session.mimeType || \"audio/webm\"),\n languageCode: String(payload?.languageCode || session.languageCode || \"tr-TR\"),\n traceId: String(payload?.traceId || session.traceId || \"\"),\n sessionId: session.sessionId,\n transport: \"gateway_ws_live\"\n })\n .then((result) => {\n appendRhizohVoiceLiveEventV0(session, result?.ok ? \"VOICE_TRANSCRIBE_FINAL\" : \"VOICE_TRANSCRIBE_FAILED\", `${session.sessionId}:final`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_FINAL, {\n ...result,\n sessionId: session.sessionId,\n transportPath: \"gateway_ws_live\",\n bytes: audio.length,\n chunkCount: session.chunks.length,\n chunkSequence: Object.freeze({\n ok: true,\n firstChunkIndex: session.chunkIndexes[0] || null,\n lastChunkIndex: session.chunkIndexes[session.chunkIndexes.length - 1] || null,\n receivedChunkCount: session.chunks.length\n }),\n eventTrace: Object.freeze((session.events || []).slice(-16)),\n recordedMs: Math.max(0, Date.now() - session.startedAtMs)\n });\n })\n .catch((e) => {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_transcribe_failed\",\n detail: String(e?.message || e)\n });\n });\n}\n\nfunction removeCastleSocialClientFromAllRooms(clientId) {\n const cid = String(clientId || \"\");\n if (!cid) return;\n for (const room of castleSocialRoomState.values()) {\n room.members.delete(cid);\n }\n}\n\nfunction buildCastleSocialRoomPayload(roomKey) {\n const room = castleSocialRoomState.get(roomKey);\n if (!room) return null;\n const now = Date.now();\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const roster = [...room.members.values()].map((m) => ({\n userId: m.userId,\n lastMs: m.lastMs,\n ...m.sp\n }));\n const peerFeeds =\n room.walPeers && room.walPeers.size\n ? [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }))\n : [];\n return { castleRoomKey: roomKey, seq: room.seq, roster, peerFeeds, ts: now };\n}\n\nconst CASTLE_WAL_PEER_MAX_FEED_BYTES = 48 * 1024;\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {{ ok: boolean, code?: string, reason?: string }} gate\n */\nfunction rejectWalPeerIngressV0(socket, gate) {\n const code = gate.code || \"wal_peer_rejected\";\n recordWalPeerRejectV0(code);\n recordSubstrateOtelEventV0(\"wal_peer_reject\", { code, reason: String(gate.reason || \"\").slice(0, 120) });\n socket.send(\n JSON.stringify(\n createEnvelope(WS_MESSAGE.ERROR, {\n error: gate.reason || \"WAL peer ingress rejected.\",\n code\n })\n )\n );\n}\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {Record<string, unknown> | null | undefined} walPeerFeed\n * @returns {boolean}\n */\nfunction gateWalPeerIngressV0(socket, walPeerFeed) {\n const authGate = assertWalPeerSocketAuthorityV0(socket);\n if (!authGate.ok) {\n rejectWalPeerIngressV0(socket, authGate);\n return false;\n }\n const sigGate = validateWalPeerFeedSignatureV0(\n walPeerFeed && typeof walPeerFeed === \"object\" ? walPeerFeed : {}\n );\n if (!sigGate.ok) {\n rejectWalPeerIngressV0(socket, sigGate);\n return false;\n }\n return true;\n}\n\nfunction handleCastleWalPeerFeed(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_WAL_PEER_MAX_FEED_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_WAL_PEER_FEED too large.\" })));\n return;\n }\n const walPeerFeed = p.walPeerFeed && typeof p.walPeerFeed === \"object\" ? p.walPeerFeed : {};\n if (!gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const castleRoomKey = String(p.castleRoomKey || \"wal:convergence\").slice(0, 64);\n const castleId = String(p.castleId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId,\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: room.members.get(socket.clientId)?.sp ?? {}\n });\n const peerFeeds = [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }));\n const body = {\n castleRoomKey,\n seq: room.seq,\n peerFeeds,\n ts: now\n };\n const env = createEnvelope(WS_MESSAGE.CASTLE_WAL_PEER_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys(), ...room.walPeers.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction handleCastleSocialPulse(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_SOCIAL_MAX_PULSE_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_SOCIAL_PULSE too large.\" })));\n return;\n }\n const castleRoomKey = String(p.castleRoomKey || \"default\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n const sp = p.socialPulse && typeof p.socialPulse === \"object\" ? p.socialPulse : {};\n const walPeerFeed =\n p.walPeerFeed && typeof p.walPeerFeed === \"object\"\n ? p.walPeerFeed\n : sp.walPeerFeed && typeof sp.walPeerFeed === \"object\"\n ? sp.walPeerFeed\n : null;\n if (walPeerFeed && !gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const spNext = walPeerFeed ? { ...sp, walPeerFeed } : { ...sp };\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: spNext\n });\n if (walPeerFeed) {\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId: String(p.castleId || userId).slice(0, 64),\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n }\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const body = buildCastleSocialRoomPayload(castleRoomKey);\n if (!body) return;\n const env = createEnvelope(WS_MESSAGE.CASTLE_SOCIAL_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcast(msgObj) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcastState() {\n broadcast(\n createEnvelope(WS_MESSAGE.BROADCAST_STATE, {\n broadcasterClientId,\n viewers: [...wss.clients].filter((c) => c.readyState === 1).length\n })\n );\n}\n\nfunction currentSpiralStatePayload() {\n return {\n rooms: spiralState.rooms,\n characters: [...spiralState.characters.values()]\n };\n}\n\nfunction broadcastSpiralState() {\n broadcast(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload()));\n}\n\nfunction ensureRoom(roomId, fallbackName = null) {\n if (!roomId) return spiralState.rooms[0].id;\n const exists = spiralState.rooms.find((r) => r.id === roomId);\n if (exists) return exists.id;\n const created = { id: roomId, name: fallbackName || `Spiral-${roomId.slice(-4)}` };\n spiralState.rooms.push(created);\n return created.id;\n}\n\nfunction syncSpiralCharactersWithWorld(snapshot) {\n const agents = snapshot?.agents || [];\n for (const a of agents) {\n if (!spiralState.characters.has(a.id)) {\n spiralState.characters.set(a.id, {\n id: a.id,\n name: a.id,\n role: \"agent\",\n roomId: spiralState.rooms[0].id,\n source: \"world\"\n });\n }\n }\n}\n\nfunction broadcastExcept(msgObj, exceptSocket) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client !== exceptSocket && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction validateInputFrame(payload, socket) {\n const stats = clientStats.get(socket.clientId) || { lastFrame: 0, windowStart: Date.now(), msgCount: 0 };\n const now = Date.now();\n if (now - stats.windowStart > 5000) {\n stats.windowStart = now;\n stats.msgCount = 0;\n }\n stats.msgCount += 1;\n clientStats.set(socket.clientId, stats);\n if (stats.msgCount > 120) return \"Rate limit exceeded.\";\n\n if (!payload || !Number.isInteger(payload.frame)) return \"frame must be integer.\";\n if (payload.frame <= stats.lastFrame) return \"frame must be monotonic.\";\n const commands = Array.isArray(payload.commands) ? payload.commands : [];\n if (commands.length > 8) return \"Too many commands in one frame.\";\n for (const cmd of commands) {\n if (!cmd || typeof cmd.command !== \"string\") return \"Invalid command format.\";\n if (\n cmd.command !== \"SPAWN_AGENT\" &&\n cmd.command !== \"LIST_CASTLES\" &&\n cmd.command !== \"SPAWN_ENTITY\" &&\n cmd.command !== \"CREATE_CASTLE\"\n ) return \"Unsupported command.\";\n }\n stats.lastFrame = payload.frame;\n clientStats.set(socket.clientId, stats);\n return null;\n}\n\nfunction validateSignalPayload(payload) {\n if (!payload || typeof payload !== \"object\") return \"Invalid signaling payload.\";\n const signalType = payload.signalType;\n if (Object.values(CASTLE_NETWORK_SIGNAL_V0).includes(signalType)) {\n return null;\n }\n if (![\"OFFER\", \"ANSWER\", \"ICE\"].includes(signalType)) return \"Invalid signal type.\";\n if (typeof payload.to !== \"string\" || payload.to.length < 3) return \"Invalid signal destination.\";\n if (signalType !== \"ICE\" && JSON.stringify(payload.sdp || {}).length > 16 * 1024) return \"SDP too large.\";\n if (signalType === \"ICE\" && JSON.stringify(payload.candidate || {}).length > 6 * 1024) return \"ICE too large.\";\n return null;\n}\n\nfunction sanitizeRawMessage(raw) {\n if (!raw) return null;\n const text = raw.toString();\n if (Buffer.byteLength(text, \"utf8\") > MAX_MESSAGE_BYTES) return null;\n return text;\n}\n\nwss.on(\"connection\", async (socket, req) => {\n const origin = req?.headers?.origin || \"\";\n if (!httpCors.isWebSocketOriginAllowed(origin)) {\n socket.close(1008, \"Origin not allowed\");\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const path = req?.url || \"\";\n const query = new URL(`ws://localhost${path}`).searchParams;\n const token = query.get(\"token\");\n if (!token || token !== REQUIRED_GATEWAY_TOKEN) {\n socket.close(1008, \"Invalid gateway token\");\n return;\n }\n }\n if (REQUIRE_AUTH) {\n const result = await verifyClientToken(req);\n if (!result.ok) {\n const isLocal = origin.includes(\"localhost\") || origin.includes(\"127.0.0.1\") || !origin;\n if (!(ALLOW_DEV_ANON && isLocal)) {\n socket.close(1008, result.reason);\n return;\n }\n socket.auth = { ok: true, kind: \"dev-anon\", user: { uid: \"dev-anon\" } };\n } else {\n socket.auth = result;\n }\n }\n\n socket.clientId = `c-${Math.random().toString(36).slice(2, 8)}`;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.HELLO, { message: \"Castle Gateway online.\", clientId: socket.clientId })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.WORLD_SNAPSHOT, orchestrator.worldSnapshot())));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.BROADCAST_STATE, { broadcasterClientId, viewers: [...wss.clients].filter((c) => c.readyState === 1).length })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload())));\n broadcastExcept(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }), socket);\n\n socket.on(\"message\", (raw) => {\n const safeRaw = sanitizeRawMessage(raw);\n if (!safeRaw) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Message too large or invalid.\" })));\n return;\n }\n const parsed = safeJsonParse(safeRaw);\n if (!parsed?.type) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid message.\" })));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND_TEXT) {\n const canonical = parseTextToCommand(parsed.payload?.text);\n if (!canonical) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Unknown command text.\" })));\n return;\n }\n const result = orchestrator.applyCommand(canonical);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.MATCH_SESSION_JOIN) {\n const joined = handleMatchSessionJoinV0(socket, parsed, wss);\n const sessionId = String(parsed.sessionId || parsed.payload?.sessionId || \"\").trim();\n if (joined?.ok && sessionId) {\n sendMatchSessionSnapshotOnJoinV0(socket, sessionId);\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.MATCH_CASTLE_INVITE) {\n handleMatchCastleInviteV0(socket, parsed, wss);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.MATCH_MOVE) {\n handleMatchMoveAuthorityV0(socket, parsed, wss);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.MATCH_STATE_APPLIED) {\n handleMatchStateAppliedV0(socket, parsed, wss);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.VOICE_STATE_APPLIED) {\n handleVoiceStateAppliedV0(socket, parsed);\n return;\n }\n\n\n if (parsed.type === WS_MESSAGE.COMMAND) {\n const result = orchestrator.applyCommand(parsed.payload);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n }\n\n if (parsed.type === WS_MESSAGE.INPUT_FRAME) {\n const err = validateInputFrame(parsed.payload, socket);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const frame = parsed.payload.frame;\n const commands = Array.isArray(parsed.payload?.commands) ? parsed.payload.commands : [];\n orchestrator.queueInputFrame(socket.clientId, frame, commands);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, queued: true, frame })));\n }\n\n if (parsed.type === WS_MESSAGE.SIGNAL) {\n const payload = parsed.payload || {};\n if (Object.values(CASTLE_NETWORK_SIGNAL_V0).includes(payload.signalType)) {\n const netErr = validateCastleNetworkSignalPayloadV0(socket, payload);\n if (netErr) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: netErr })));\n return;\n }\n handleCastleNetworkSignalV0(socket, payload, wss);\n return;\n }\n const err = validateSignalPayload(payload);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const signalType = payload.signalType;\n const to = payload.to;\n for (const client of wss.clients) {\n if (client.clientId === to && client.readyState === 1) {\n client.send(JSON.stringify(createEnvelope(WS_MESSAGE.SIGNAL, { ...payload, from: socket.clientId })));\n break;\n }\n }\n }\n\n if (parsed.type === WS_MESSAGE.DRONE_TELEMETRY) {\n const payload = parsed.payload || {};\n const json = JSON.stringify(payload);\n if (Buffer.byteLength(json, \"utf8\") > 48 * 1024) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"DRONE_TELEMETRY payload too large.\" })));\n return;\n }\n broadcastExcept(createEnvelope(WS_MESSAGE.DRONE_TELEMETRY, { ...payload, relayFrom: socket.clientId }), socket);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_START) {\n handleRhizohVoiceLiveStartV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_CHUNK) {\n handleRhizohVoiceLiveChunkV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_STOP) {\n handleRhizohVoiceLiveStopV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_SOCIAL_PULSE) {\n handleCastleSocialPulse(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_WAL_PEER_FEED) {\n handleCastleWalPeerFeed(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.OPEN_DATA_QUERY) {\n queryOpenData(parsed.payload)\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: true, result })));\n })\n .catch((e) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: false, error: e.message })));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_QUERY) {\n runRhizohBrainV2({\n ...(parsed.payload || {}),\n uid: socket?.auth?.user?.uid || \"unknown\"\n })\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, result)));\n })\n .catch(async () => {\n const fallback = await runRhizohBrain(parsed.payload || {}).catch(() => ({\n ok: false,\n reply: \"Rhizoh temporary disturbance. Habitat remains stable.\"\n }));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, fallback)));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.BROADCAST_REGISTER) {\n const role = String(parsed.payload?.role || \"\");\n const serviceKind = String(parsed.payload?.kind || parsed.payload?.serviceKind || \"\").trim();\n const serviceId = String(parsed.payload?.serviceId || role || socket.clientId || \"\").trim();\n if (serviceKind && serviceId) {\n registerGatewayServiceNodeV0({\n kind: serviceKind,\n serviceId,\n gatewayClientId: socket.clientId,\n state: String(parsed.payload?.state || \"ONLINE\"),\n meta: parsed.payload?.meta\n });\n }\n if (role === \"GENESIS_BROADCAST_AGENT\") {\n broadcasterClientId = socket.clientId;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, registered: role })));\n broadcastState();\n } else if (serviceKind) {\n socket.send(\n JSON.stringify(\n createEnvelope(WS_MESSAGE.COMMAND_RESULT, {\n ok: true,\n registered: serviceKind,\n serviceId,\n interpretationOnly: true\n })\n )\n );\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.STUDIO_CUE) {\n // Only registered broadcaster can push global cues.\n if (!broadcasterClientId || socket.clientId !== broadcasterClientId) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Only broadcaster agent can emit STUDIO_CUE.\" })));\n return;\n }\n const cue = parsed.payload || {};\n if (typeof cue !== \"object\") {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid STUDIO_CUE payload.\" })));\n return;\n }\n broadcast(createEnvelope(WS_MESSAGE.STUDIO_CUE, cue));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_JOIN_ROOM) {\n const requested = String(parsed.payload?.roomId || \"\").trim();\n const requestedName = String(parsed.payload?.roomName || \"\").trim();\n const roomId = ensureRoom(requested || `room-${Math.random().toString(36).slice(2, 7)}`, requestedName || null);\n spiralState.activeByClient.set(socket.clientId, roomId);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, roomId })));\n broadcastSpiralState();\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_CREATE_CHARACTER) {\n const name = String(parsed.payload?.name || \"\").trim().slice(0, 32) || `AI-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;\n const role = String(parsed.payload?.role || \"agent\").trim().slice(0, 24) || \"agent\";\n const roomId = ensureRoom(\n String(parsed.payload?.roomId || spiralState.activeByClient.get(socket.clientId) || spiralState.rooms[0].id),\n null\n );\n\n // Character creation always maps to an authoritative world agent.\n const result = orchestrator.applyCommand({\n command: COMMAND.SPAWN_AGENT,\n params: { targetCastleId: \"castle-01\" }\n });\n if (!result?.ok || !result?.data?.id) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Character creation failed.\" })));\n return;\n }\n const id = result.data.id;\n spiralState.characters.set(id, { id, name, role, roomId, source: \"spawned\" });\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, characterId: id, roomId })));\n broadcastSpiralState();\n return;\n }\n });\n\n socket.on(\"close\", () => {\n if (socket.clientId === broadcasterClientId) broadcasterClientId = null;\n leaveMatchBroadcastRoomV0(socket);\n unregisterGatewayServiceNodesByClientV0(socket.clientId);\n clientStats.delete(socket.clientId);\n rhizohVoiceLiveSessions.delete(socket);\n spiralState.activeByClient.delete(socket.clientId);\n removeCastleSocialClientFromAllRooms(socket.clientId);\n removeCastleNetworkClientV0(socket.clientId, wss);\n broadcast(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }));\n broadcastState();\n });\n});\n\nsetInterval(() => {\n void (async () => {\n try {\n const { frame, snapshot, delta, packedDelta } = orchestrator.tick(1 / 20);\n syncSpiralCharactersWithWorld(snapshot);\n broadcast(createEnvelope(WS_MESSAGE.NETWORK_TICK, { frame }));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_TICK, snapshot));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA, delta));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA_PACKED, packedDelta));\n broadcastSpiralState();\n await runAcademyEventTick();\n const executed = await tickAutomations(Date.now());\n for (const a of executed) {\n await addTranscript(a.uid, {\n source: \"planner\",\n eventType: \"automation-executed\",\n text: `Automation executed: ${a.title} (${a.action})`,\n roomId: \"studio-main\",\n meta: { automationId: a.id, action: a.action, deviceId: a.deviceId || \"\" }\n });\n }\n } catch (e) {\n console.error(\"[GATEWAY] world tick / automations loop error:\", String(e?.message || e));\n }\n })();\n}, 50);\n\n(async () => {\n await rhizohRuntime.telemetry.initOpenTelemetry();\n if (String(process.env.CASTLE_GENESIS_DISK_PERSIST ?? \"\").trim() === \"\") {\n process.env.CASTLE_GENESIS_DISK_PERSIST = \"1\";\n }\n const genesisDiskBoot = await hydrateGenesisContinuityPersistenceBootV0();\n if (!genesisDiskBoot.ok) {\n console.warn(\"[GATEWAY] genesis disk hydrate failed; continuing with in-memory genesis continuity\");\n }\n httpServer.listen(PORT, () => {\n startGenesisCanonicalClock();\n installGenesisCheckpointSurfaceGetter(buildGenesisRuntimeSurfacePayloadLive);\n setGenesisContinuityAfterPublishHook((seq) => {\n noteGenesisCheckpointSeqCommitted(seq);\n noteGenesisContinuitySeqAcceptedV0(seq);\n });\n startGenesisContinuityInfraSampler(buildGenesisRuntimeSurfacePayloadLive, 2000);\n logProductionObservatorySurfaceGuardsV0();\n logGatewaySubstrateAuthorityGuardsV0();\n if (isLlmWorkerEnabledV0()) {\n try {\n const workerBoot = initLlmWorkerAtBootV0();\n console.log(\"[GATEWAY] llm worker boot:\", workerBoot);\n } catch (workerErr) {\n console.error(\"[GATEWAY] llm worker boot failed:\", workerErr);\n }\n }\n console.log(`[GATEWAY] ws/http://localhost:${PORT}`);\n console.log(\n `[GATEWAY] genesis continuity: GET ${rhizohRuntime.routes.genesisRuntime} | POST ${rhizohRuntime.routes.genesisIngress} | SSE ${rhizohRuntime.routes.genesisStream} | replay ${rhizohRuntime.routes.genesisReplay} | diff ${rhizohRuntime.routes.genesisReplayDiff} | equiv ${rhizohRuntime.routes.genesisReplayEquivalence} | analytics ${rhizohRuntime.routes.genesisReplayAnalytics} | evolution ${rhizohRuntime.routes.genesisReplayEvolution}`\n );\n if (String(process.env.CASTLE_GENESIS_OBSERVATORY_LIVE_V01 ?? \"\").trim() === \"1\") {\n console.log(\"GENESIS OBSERVATORY LIVE v0.1 — READ ONLY PHASE STARTED\");\n }\n });\n})().catch((e) => {\n console.error(\"[GATEWAY] fatal boot (before or during listen):\", e);\n process.exit(1);\n});\n"}, "6ad6064fa7feb4ce980f3a0ddce51871fb117d54": {"t": 1785754114.9229426, "n": 179840, "h": "c0fcfe4e326b826b", "c": "import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport dotenv from \"dotenv\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconsole.log(\"🔥 [GENESIS_BOOT] entry file:\", __filename);\nconsole.log(\"🔥 [GENESIS_BOOT] node env:\", process.env.NODE_ENV);\nconsole.log(\"🔥 [GENESIS_BOOT] pid:\", process.pid);\nconsole.log(\"🧬 [GENESIS_BOOT] cwd:\", process.cwd());\nconsole.log(\"🧬 [GENESIS_BOOT] import.meta.url:\", import.meta.url);\ndotenv.config({ path: path.join(__dirname, \"..\", \".env\") });\ndotenv.config({ path: path.join(__dirname, \"..\", \".env.local\"), override: true });\n\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport { createServer } from \"node:http\";\nimport { createOrchestrator } from \"@castle/orchestrator\";\nimport { parseTextToCommand } from \"@castle/command-dsl\";\nimport {\n WS_MESSAGE,\n COMMAND,\n createEnvelope,\n createRhizohImmutableEventV0,\n createRhizohPayloadRefV0,\n safeJsonParse,\n validateRhizohVoiceLiveChunkPayloadV0\n} from \"@castle/protocol\";\nimport { queryOpenData } from \"./openData.js\";\nimport { verifyClientToken } from \"./auth.js\";\nimport { handleMatchMoveAuthorityV0, leaveMatchBroadcastRoomV0, sendMatchSessionSnapshotOnJoinV0 } from \"./rhizoh/matchMoveAuthorityV0.js\";\nimport { handleMatchSessionJoinV0, handleMatchCastleInviteV0 } from \"./rhizoh/matchBroadcastRoomV0.js\";\nimport { handleMatchStateAppliedV0 } from \"./rhizoh/matchAckAggregatorV0.js\";\nimport {\n listUnifiedGatewayPresenceV0,\n registerGatewayServiceNodeV0,\n unregisterGatewayServiceNodesByClientV0\n} from \"./rhizoh/gatewayPresenceRegistryV0.js\";\nimport {\n enrichVoiceLivePayloadV0,\n handleVoiceStateAppliedV0,\n registerVoiceGatewayCitizenV0,\n resolveVoiceWorldContextV0,\n sendVoiceTranscriptCommittedV0,\n} from \"./rhizoh/voiceGatewayCitizenshipV0.js\";\nimport { queryRhizohLlm } from \"./rhizohLlmGateway.js\";\nimport { rhizohGatewayTurn } from \"./rhizohGatewayTurn.js\";\nimport {\n enqueueLlmWorkerTaskAsyncV0,\n getLatestLlmWorkerTaskIdV0,\n getLlmWorkerDebugSnapshotV0,\n getLlmWorkerTaskSnapshotV0,\n initLlmWorkerAtBootV0,\n isLlmWorkerAsyncEnabledV0,\n isLlmWorkerEnabledV0,\n runLlmWorkerTaskV0\n} from \"./llmWorkerRuntime.js\";\nimport {\n RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n runRhizohVoiceTranscribeV3,\n rhizohVoiceTranscribeEnvV3\n} from \"./rhizohVoiceTranscribeV3.js\";\nimport { createHttpCorsPolicy, normalizeHttpOrigin, parseAllowedOriginsFromEnv } from \"./httpCorsPolicyV1.js\";\nimport {\n applyLanguagePropagationToLlmPayloadV1,\n applyLanguagePropagationToVoiceBodyV1,\n buildRhizohLanguagePropagationEchoV1,\n parseRhizohLanguagePropagationV1\n} from \"./rhizohLanguagePropagationV1.js\";\nimport { trimRhizohLlmGatewayPayloadV1 } from \"./rhizohLlmPayloadTrimV1.js\";\nimport {\n meshAppendDelta,\n meshContinuityAggregate,\n meshJoin,\n meshLeave,\n meshReplayFromNodeId,\n meshReplayFromSeq,\n meshSnapshot,\n meshSubscribeSse\n} from \"./presenceRoomMesh.js\";\nimport { runRhizohBrainV2 } from \"./rhizohBrainV2.js\";\nimport {\n listConnections,\n createConnection,\n updateConnection,\n deleteConnection,\n setDefaultConnection,\n resolveConnection\n} from \"./llmConnectionsStore.js\";\nimport { getCloudSyncSnapshotV0, mergeCloudSyncSnapshotV0 } from \"./castleCloudSyncStore.js\";\nimport {\n CASTLE_NETWORK_SIGNAL_V0,\n handleCastleNetworkSignalV0,\n listCastleNetworkPresenceV0,\n removeCastleNetworkClientV0,\n validateCastleNetworkSignalPayloadV0\n} from \"./castleNetworkRelayV0.js\";\nimport { checkHttpRateLimit, getHttpClientIp } from \"./castleHttpRateLimit.js\";\nimport {\n submitAbuseReportV0,\n listModerationQueueV0,\n softBlockUserV0,\n removeSoftBlockV0,\n listSoftBlockedUsersV0\n} from \"./ops/moderationMvpV0.js\";\nimport { listRecentAgentSnapshotsV0 } from \"./ops/agentObservabilityV0.js\";\nimport { readAgentContainmentConfigV0 } from \"./ops/agentContainmentV0.js\";\nimport { readCostContainmentConfigV0, getCostContainmentStatsV0 } from \"./ops/costContainmentV0.js\";\nimport { getGlobalCostLedgerSnapshotV0, getGclBackendStatusV0 } from \"./ops/globalCostLedgerV0.js\";\nimport { evaluateEconomicStrategyL3V0 } from \"./ops/economicStrategyEngineL3V0.js\";\nimport { getPhasedRolloutStatsV0, readPhasedRolloutTierV0 } from \"./ops/phasedRolloutV0.js\";\nimport { buildUnifiedStateNarrativeV0 } from \"./ops/unifiedStateNarrativeV0.js\";\nimport { resolveHardeningTenantIdV0 } from \"./ops/narrativeTenantIsolationV0.js\";\nimport {\n classifyStressResponseV0,\n verifyStressTaxonomyCoverageV0,\n verifyStressConflictResolutionV0\n} from \"./ops/stressResponseTaxonomyV0.js\";\nimport { verifyResolutionStabilityEnvelopeV0 } from \"./ops/resolutionStabilityEnvelopeV0.js\";\nimport { logLlmAccess } from \"./castleLlmAudit.js\";\nimport { appendMemory, listMemories, getMemoryContext, getPersonaGoalMemory, setPersonaGoalMemory, autoCompactMemories } from \"./memoryStore.js\";\nimport { getFirebasePersistence } from \"./firebasePersistence.js\";\nimport { registerAgentIdentity, listAgentIdentities, getAgentIdentity, updateAgentIdentity } from \"./agentIdentityStore.js\";\nimport { queueAcademyEvent, listAcademyEvents, resolveAcademyEvent, runAcademyEventTick } from \"./academyEventEngine.js\";\nimport { enqueuePdfJob, getPdfJob } from \"./pdfPipeline.js\";\nimport {\n createPublishSession,\n listPublishSessions,\n updatePublishSession,\n addTranscript,\n listTranscripts,\n searchMetadata,\n registerDevice,\n listDevices,\n updateDevice,\n appendTelemetry,\n listTelemetry,\n createAutomation,\n listAutomations,\n tickAutomations,\n enqueueCommand,\n listCommands,\n updateCommandStatus,\n registerSocialChannel,\n listSocialChannels\n} from \"./studioOpsStore.js\";\nimport { metrics as infraMetrics } from \"./infra/metrics.js\";\nimport { renderRhizohPrometheusMetrics, rhizohEnterpriseMetrics } from \"./infra/rhizohEnterpriseMetrics.js\";\nimport { scoreHealth } from \"./infra/healthScore.js\";\nimport { getTraceById, listTracesBySession } from \"./infra/traceRegistry.js\";\nimport { canonicalEpistemicSealString, hashAndSignEpistemicSeal, EPISTEMIC_SEAL_SCHEMA } from \"./epistemicSeal.js\";\nimport { persistEpistemicLedgerBatch } from \"./epistemicLedgerStore.js\";\nimport { persistEpistemicForecastBatch } from \"./epistemicForecastStore.js\";\nimport {\n persistAuthorityLedgerWitnessBatchV1,\n getAuthorityLedgerWitnessSnapshotV1,\n replayAuthorityLedgerWitnessChainV1\n} from \"./authorityLedgerWitnessStoreV1.js\";\nimport { assimilateAuthorityEpochMergeV1 } from \"./authorityEpochMergeStoreV1.js\";\nimport { buildRhizohExternalGroundTruthPayload } from \"./rhizohExternalGroundTruthGateway.js\";\nimport { ingestRhizohExternalLossBatchHttp } from \"./rhizohExternalLossIngestGateway.js\";\nimport {\n ingestRhizohProductOutcomeHttp,\n listRhizohProductOutcomeAggregates,\n verifyRhizohOutcomeHmac,\n verifyRhizohOutcomeSourceToken\n} from \"./rhizohProductOutcomeIngestGateway.js\";\nimport { initRhizoh } from \"./rhizohProductionBootstrap.js\";\nimport { handleAcademicObservatoryExportGetV0 } from \"./rhizoh/academicObservatoryHttpV0.js\";\nimport { handleCouncilAnomalyReasoningPostV0 } from \"./council/rhizohEpistemicCouncilHttpV0.js\";\nimport {\n handleLiveSportsBundleGetV0,\n handleLiveNewsHeadlinesGetV0,\n handleLiveWorldFeedGetV0\n} from \"./rhizoh/liveWorldFeedHttpV0.js\";\nimport {\n buildGenesisRuntimeSurfacePayload,\n recordGenesisEpistemicLedgerPersisted,\n recordGenesisEpistemicSealIssued,\n startGenesisCanonicalClock\n} from \"./genesisRuntimeSurfaceV0.js\";\nimport { registerGenesisSseClient, getGenesisContinuitySeq, setGenesisContinuityAfterPublishHook } from \"./genesisContinuityStreamHubV0.js\";\nimport { computeGenesisReplayFingerprintV0 } from \"./genesisReplayFingerprintV0.js\";\nimport { buildGenesisGatewayCapabilitiesV0 } from \"./genesisGatewayCapabilitiesV0.js\";\nimport {\n installGenesisCheckpointSurfaceGetter,\n noteGenesisCheckpointSeqCommitted,\n getLatestGenesisSignedCheckpoint\n} from \"./genesisContinuityCheckpointV0.js\";\nimport { hydrateGenesisContinuityPersistenceBootV0 } from \"./genesisContinuityHydrateBootV0.js\";\nimport { startGenesisContinuityInfraSampler } from \"./genesisContinuityInfraSamplerV0.js\";\nimport {\n genesisCheckpointQueryBySeqV0,\n genesisCheckpointQueryRangeV0,\n genesisCheckpointQueryLineageV0\n} from \"./genesisCheckpointQueryV0.js\";\nimport {\n genesisContinuityEventArchiveEnabled,\n queryGenesisContinuityEventArchiveV0,\n GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION\n} from \"./genesisContinuityEventArchiveV0.js\";\nimport { resolveGenesisReplayRouterV1, GENESIS_REPLAY_ROUTER_SCHEMA } from \"./genesisReplayRouterV1.js\";\nimport {\n computeGenesisReplayTemporalDiffV1,\n GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA\n} from \"./genesisReplayTemporalDiffV1.js\";\nimport { compareGenesisReplayEquivalenceV1 } from \"./genesisReplayEquivalenceV1.js\";\nimport { computeGenesisReplayAnalyticsV1 } from \"./genesisReplayAnalyticsV1.js\";\nimport { computeGenesisReplayEvolutionV1 } from \"./genesisReplayEvolutionV1.js\";\nimport { handleStripeCheckoutCreate, handleStripeMembershipWebhook } from \"./stripeWebhookMembership.js\";\nimport { buildRealityHealthPayload } from \"./edgeRealityConsistencyV1.js\";\nimport {\n assertWalPeerSocketAuthorityV0,\n buildGatewaySubstrateAuthoritySnapshotV0,\n logGatewaySubstrateAuthorityGuardsV0,\n validateWalPeerFeedSignatureV0\n} from \"./gatewaySubstrateAuthorityV0.js\";\nimport {\n buildSubstrateOperationalSnapshotV0,\n recordWalPeerRejectV0,\n renderSubstratePrometheusMetricsV0\n} from \"./infra/substrateOperationalMetrics.js\";\nimport { ingestClientSubstrateHealthV0 } from \"./rhizohSubstrateHealthIngestV0.js\";\nimport { recordSubstrateOtelEventV0 } from \"./infra/opentelemetrySubstrateV0.js\";\nimport {\n GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n getGenesisWorldObservationIngressStatusV0,\n ingestGenesisWorldObservationV0\n} from \"./genesisContinuityClientIngressV0.js\";\nimport {\n getGenesisContinuitySeqAuditV0,\n noteGenesisContinuitySeqAcceptedV0\n} from \"./genesisContinuitySeqGapV0.js\";\n\n/** Render / Fly / Railway: `PORT` — yoksa `CASTLE_GATEWAY_PORT` — yoksa 8090. */\nconst PORT =\n Number(process.env.PORT) ||\n Number(process.env.CASTLE_GATEWAY_PORT) ||\n 8090;\n/** Spiral / world WebSocket server — set after `wss` init; used by genesis runtime surface. */\nlet spiralWssForGenesis = null;\nconst MAX_MESSAGE_BYTES = Number(process.env.CASTLE_MAX_MESSAGE_BYTES || 32 * 1024);\nconst REQUIRED_GATEWAY_TOKEN = process.env.CASTLE_GATEWAY_TOKEN || \"\";\nconst ALLOWED_ORIGINS = parseAllowedOriginsFromEnv().fromList;\nconst httpCors = createHttpCorsPolicy();\n\n/** @returns {string|null} `Access-Control-Allow-Origin`; null = browser CORS fail. */\nfunction accessControlAllowOriginValue(req) {\n return httpCors.accessControlAllowOriginValue(req);\n}\n\nfunction applyHttpCorsHeaders(req, res) {\n return httpCors.applyHttpCorsHeaders(req, res);\n}\nconst REQUIRE_AUTH = process.env.CASTLE_REQUIRE_AUTH === \"true\";\nconst ALLOW_DEV_ANON = process.env.CASTLE_ALLOW_DEV_ANON !== \"false\";\nconst ALLOW_DEV_HTTP_UID = process.env.CASTLE_ALLOW_DEV_HTTP_UID !== \"false\";\n/** Presence mesh: allow `X-Castle-Guest-Id` when Firebase auth absent (default on). Set `false` to require auth only. */\nconst ALLOW_MESH_GUEST = process.env.CASTLE_ALLOW_MESH_GUEST !== \"false\";\nconst SOCIAL_RETRY_MAX = Math.max(0, Math.min(5, Number(process.env.CASTLE_SOCIAL_RETRY_MAX || 2)));\nconst SOCIAL_RETRY_BASE_MS = Math.max(100, Number(process.env.CASTLE_SOCIAL_RETRY_BASE_MS || 700));\nconst TELEGRAM_BOT_TOKEN = process.env.CASTLE_TELEGRAM_BOT_TOKEN || \"\";\nconst WHATSAPP_TOKEN = process.env.CASTLE_WHATSAPP_TOKEN || \"\";\nconst WHATSAPP_PHONE_NUMBER_ID = process.env.CASTLE_WHATSAPP_PHONE_NUMBER_ID || \"\";\nconst RL_RHIZOH_LLM_PER_MIN = Math.max(5, Number(process.env.CASTLE_RL_RHIZOH_LLM_PER_MIN || 40));\nconst RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN || 30)\n);\nconst RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN || 120));\nconst RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN = Math.max(\n 10,\n Number(process.env.CASTLE_RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN || 60)\n);\nconst RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN = Math.max(8, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN || 48));\nconst RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN = Math.max(4, Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN || 24));\nconst RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN || 18)\n);\nconst RL_EPISTEMIC_SEAL_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_EPISTEMIC_SEAL_PER_MIN || 120));\nconst RL_EPISTEMIC_LOG_PER_MIN = Math.max(20, Number(process.env.CASTLE_RL_EPISTEMIC_LOG_PER_MIN || 240));\nconst EPISTEMIC_SEAL_SECRET = String(\n process.env.CASTLE_EPISTEMIC_SEAL_SECRET || process.env.CASTLE_GATEWAY_TOKEN || \"\"\n).trim();\nconst RL_LLM_CONN_TEST_PER_MIN = Math.max(2, Number(process.env.CASTLE_RL_LLM_CONNECTION_TEST_PER_MIN || 8));\n\n/** One-shot production warnings: CORS surface, auth toggle, dev bypass flags (observatory deploy hygiene). */\nfunction logProductionObservatorySurfaceGuardsV0() {\n if (String(process.env.NODE_ENV || \"\").toLowerCase() !== \"production\") return;\n const tag = \"[GATEWAY_PROD_GUARD]\";\n const primaryCors = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n const corsListEmpty = ALLOWED_ORIGINS.length === 0;\n if (corsListEmpty && !primaryCors) {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty and CASTLE_HTTP_CORS_ORIGIN empty — production still merges RHIZOH_BROWSER_ORIGINS_V1 defaults; set env for preview/staging hosts`\n );\n } else if (corsListEmpty && primaryCors && primaryCors !== \"*\") {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty — only CASTLE_HTTP_CORS_ORIGIN set; add web.app + firebaseapp.com (and previews) to CASTLE_ALLOWED_ORIGINS if clients use alternate hostnames`\n );\n }\n if (primaryCors && /localhost|127\\.0\\.0\\.1/i.test(primaryCors)) {\n console.warn(`${tag} CORS: CASTLE_HTTP_CORS_ORIGIN is localhost while NODE_ENV=production`);\n }\n if (REQUIRE_AUTH) {\n console.warn(\n `${tag} AUTH: CASTLE_REQUIRE_AUTH=true — clients must send a valid gateway credential (otherwise silent 401 on replay/SSE); EventSource cannot set custom headers unless proxied`\n );\n if (REQUIRED_GATEWAY_TOKEN.length < 16) {\n console.warn(`${tag} AUTH: CASTLE_GATEWAY_TOKEN missing or short while require-auth enabled`);\n }\n }\n if (ALLOW_DEV_ANON) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_ANON is not \"false\" — dev anonymous path may be live in production (set CASTLE_ALLOW_DEV_ANON=false)`\n );\n }\n if (ALLOW_DEV_HTTP_UID) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_HTTP_UID is not \"false\" — X-Castle-Dev-Uid bypass risk (set CASTLE_ALLOW_DEV_HTTP_UID=false)`\n );\n }\n}\n\nconst rhizohRuntime = initRhizoh();\n\n/** İstemci: llmKeySource veya keySource — env | user_connection | auto */\nfunction normalizeClientLlmKeySource(raw) {\n const s = String(raw ?? \"\").trim().toLowerCase();\n if (s === \"env\" || s === \"server\") return \"env\";\n if (s === \"user_connection\" || s === \"connection\" || s === \"user\") return \"user_connection\";\n if (s === \"auto\" || s === \"\") return \"auto\";\n return null;\n}\n\n/**\n * Server-side provider wins for env/auto unless user_connection — client bundle may still send provider=openai.\n * @param {\"env\"|\"user_connection\"|\"auto\"} keyMode\n * @param {{ provider?: string, model?: string, apiKey?: string } | null} conn\n * @param {{ provider?: string, model?: string }} payload\n */\nfunction resolveServerLlmProviderV0(keyMode, conn, payload) {\n const envProvider = String(process.env.CASTLE_LLM_PROVIDER || \"\").trim() || undefined;\n const envModel = String(process.env.CASTLE_LLM_MODEL || \"\").trim() || undefined;\n\n if (keyMode === \"user_connection\") {\n return {\n resolvedProvider: conn?.provider || payload?.provider,\n resolvedModel: conn?.model || payload?.model,\n connApiKey: conn?.apiKey || \"\"\n };\n }\n\n if (keyMode === \"env\") {\n return {\n resolvedProvider: envProvider ?? payload?.provider,\n resolvedModel: envModel ?? payload?.model,\n connApiKey: \"\"\n };\n }\n\n if (conn?.provider && conn?.apiKey) {\n return {\n resolvedProvider: conn.provider,\n resolvedModel: conn.model,\n connApiKey: conn.apiKey\n };\n }\n\n return {\n resolvedProvider: envProvider ?? payload?.provider,\n resolvedModel: envModel ?? payload?.model,\n connApiKey: \"\"\n };\n}\n\nfunction jsonReplacerSafeV0(_key, value) {\n if (typeof value === \"bigint\") return Number(value);\n return value;\n}\n\nfunction sendJson(res, statusCode, payload) {\n res.writeHead(statusCode, { \"Content-Type\": \"application/json; charset=utf-8\" });\n try {\n res.end(JSON.stringify(payload, jsonReplacerSafeV0));\n } catch (error) {\n res.end(\n JSON.stringify({\n ok: false,\n error: \"response_serialize_failed\",\n detail: String(error?.message || error)\n })\n );\n }\n}\n\nfunction logRhizohHealth(stage, detail = {}) {\n try {\n console.info(`[CASTLE_${String(stage || \"unknown\")}]`, detail && typeof detail === \"object\" ? detail : {});\n } catch {\n /* noop */\n }\n}\n\nfunction renderInfraMetricsProm() {\n const lines = [\n `castle_gateway_events_processed_total ${infraMetrics.eventsProcessed}`,\n `castle_gateway_duplicate_reject_total ${infraMetrics.duplicateRejects}`,\n `castle_gateway_enqueue_latency_ms ${infraMetrics.enqueueLatencyMs}`,\n `castle_gateway_queue_depth ${infraMetrics.queueDepth}`,\n `castle_gateway_queue_lag_seconds ${(Number(infraMetrics.queueLag || 0) / 1000).toFixed(3)}`,\n `castle_gateway_errors_total ${infraMetrics.errors}`\n ];\n const buckets = infraMetrics.enqueueLatencyBuckets || {};\n for (const k of Object.keys(buckets)) {\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"${k}\"} ${buckets[k]}`);\n }\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"+Inf\"} ${infraMetrics.eventsProcessed}`);\n if (process.env.CASTLE_SUBSTRATE_METRICS !== \"0\") {\n lines.push(renderSubstratePrometheusMetricsV0().trimEnd());\n }\n return lines.join(\"\\n\");\n}\n\nasync function fetchJsonWithTimeout(url, timeoutMs = 1200) {\n const ctl = new AbortController();\n const tid = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const res = await fetch(url, { signal: ctl.signal });\n if (!res.ok) return null;\n return await res.json();\n } catch {\n return null;\n } finally {\n clearTimeout(tid);\n }\n}\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction getHttpPathname(req) {\n let p = String(req.url || \"\").split(\"?\")[0];\n /* Reverse proxies / clients sometimes append `/` — avoid silent 404 on e.g. `/rhizoh/genesis/stream/`. */\n while (p.length > 1 && p.endsWith(\"/\")) {\n p = p.slice(0, -1);\n }\n return p;\n}\n\nfunction rhizohLlmEnvConfigured() {\n const p = String(process.env.CASTLE_LLM_PROVIDER || \"openai\").toLowerCase();\n const key =\n p === \"anthropic\"\n ? process.env.ANTHROPIC_API_KEY\n : p === \"gemini\"\n ? process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY\n : p === \"xai\"\n ? process.env.XAI_API_KEY\n : p === \"deepseek\"\n ? process.env.DEEPSEEK_API_KEY\n : p === \"mistral\"\n ? process.env.MISTRAL_API_KEY\n : p === \"openrouter\"\n ? process.env.OPENROUTER_API_KEY\n : process.env.OPENAI_API_KEY;\n return !!String(key || \"\").trim();\n}\n\n/**\n * @param {import(\"firebase-admin/firestore\").Firestore | null} db\n * @param {number} timeoutMs\n */\nasync function probeFirestoreReachable(db, timeoutMs = 2000) {\n if (!db) return { ok: false, reason: \"no_db\" };\n const timeout = sleep(timeoutMs).then(() => ({ ok: false, reason: \"timeout\" }));\n const work = db\n .collection(\"_castle_gateway_health\")\n .doc(\"ping\")\n .get()\n .then(() => ({ ok: true, reason: \"reachable\" }))\n .catch((e) => ({ ok: false, reason: String(e?.message || e || \"firestore_error\") }));\n return Promise.race([work, timeout]);\n}\n\nfunction stripHtml(text = \"\") {\n return String(text)\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \" \")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \" \")\n .replace(/<[^>]+>/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction haversineMeters(aLat, aLon, bLat, bLon) {\n const R = 6371000;\n const dLat = ((bLat - aLat) * Math.PI) / 180;\n const dLon = ((bLon - aLon) * Math.PI) / 180;\n const sa = Math.sin(dLat / 2);\n const sb = Math.sin(dLon / 2);\n const x = sa * sa + Math.cos((aLat * Math.PI) / 180) * Math.cos((bLat * Math.PI) / 180) * sb * sb;\n return 2 * R * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));\n}\n\nasync function checkPublishTargetHealth(protocol, target) {\n const p = String(protocol || \"\").toUpperCase();\n const t = String(target || \"\").trim();\n if (!t) return { ok: false, reason: \"target_missing\", shouldReconnect: false };\n if (p === \"RTMP\") {\n const ok = t.startsWith(\"rtmp://\") || t.startsWith(\"rtmps://\");\n return { ok, reason: ok ? \"rtmp_target_format_ok\" : \"invalid_rtmp_target\", shouldReconnect: !ok, reconnectInSec: 15 };\n }\n if (p === \"WHIP\" || p === \"SFU\") {\n try {\n const res = await fetch(t, { method: \"OPTIONS\" });\n const ok = res.status < 500;\n return { ok, reason: `http_${res.status}`, shouldReconnect: !ok, reconnectInSec: 8 };\n } catch {\n return { ok: false, reason: \"network_unreachable\", shouldReconnect: true, reconnectInSec: 8 };\n }\n }\n return { ok: true, reason: \"unchecked_protocol\", shouldReconnect: false };\n}\n\nasync function fetchTextFromUrl(url) {\n const res = await fetch(url, { method: \"GET\" });\n if (!res.ok) throw new Error(`fetch_${res.status}`);\n const ct = String(res.headers.get(\"content-type\") || \"\");\n if (ct.includes(\"application/pdf\")) throw new Error(\"pdf_binary_not_supported_directly\");\n const body = await res.text();\n return stripHtml(body).slice(0, 14000);\n}\n\nfunction buildTaskActionGraph(task = \"\", context = {}) {\n const t = String(task || \"\").toLowerCase();\n if (t.includes(\"dinner\") || t.includes(\"akşam yemeği\") || t.includes(\"aksam yemegi\")) {\n return {\n taskTitle: \"Dinner Prep Planner\",\n nodes: [\n { id: \"n1\", title: \"Kitchen preheat\", action: \"smart-oven.preheat\", params: { temperatureC: 190, mode: \"fan\" } },\n { id: \"n2\", title: \"Lighting comfort\", action: \"smart-light.set_scene\", params: { scene: \"warm_dinner\" } },\n { id: \"n3\", title: \"Fridge inventory check\", action: \"smart-fridge.inventory_scan\", params: {} },\n { id: \"n4\", title: \"Reminder notification\", action: \"smart-watch.notify\", params: { text: \"Dinner prep started.\" } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" },\n { from: \"n3\", to: \"n4\" }\n ]\n };\n }\n return {\n taskTitle: task || \"General Planner\",\n nodes: [\n { id: \"n1\", title: \"Safety check\", action: \"device.safety_check\", params: { context } },\n { id: \"n2\", title: \"Primary action\", action: \"device.execute_task\", params: { task } },\n { id: \"n3\", title: \"Report\", action: \"studio.timeline_note\", params: { task } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" }\n ]\n };\n}\n\nfunction buildEthicsPrograms() {\n return {\n policyVersion: \"v1\",\n tracks: [\n {\n id: \"elderly-care-assist\",\n audience: \"elderly\",\n title: \"Elderly Assistance Track\",\n principles: [\"human-in-the-loop\", \"consent-first\", \"low-friction alerts\"],\n modules: [\"medication-reminder\", \"fall-risk-check\", \"family-notify\", \"daily-companion-brief\"]\n },\n {\n id: \"kids-safe-learning\",\n audience: \"children\",\n title: \"Kids Safe Learning Track\",\n principles: [\"guardian-consent\", \"age-appropriate-language\", \"content-filtering\"],\n modules: [\"study-coach\", \"screen-time-balance\", \"safe-social-share\", \"emergency-contact\"]\n }\n ]\n };\n}\n\nasync function deliverViaTelegram(channel, text) {\n const chatId = String(channel.endpoint || \"\").trim();\n if (!TELEGRAM_BOT_TOKEN) throw new Error(\"telegram_token_missing\");\n if (!chatId) throw new Error(\"telegram_chat_id_missing\");\n const url = `https://api.telegram.org/bot${encodeURIComponent(TELEGRAM_BOT_TOKEN)}/sendMessage`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ chat_id: chatId, text })\n });\n if (!res.ok) throw new Error(`telegram_http_${res.status}`);\n return { provider: \"telegram\", status: res.status };\n}\n\nasync function deliverViaWhatsapp(channel, text) {\n const to = String(channel.endpoint || \"\").trim();\n if (!WHATSAPP_TOKEN) throw new Error(\"whatsapp_token_missing\");\n if (!WHATSAPP_PHONE_NUMBER_ID) throw new Error(\"whatsapp_phone_number_id_missing\");\n if (!to) throw new Error(\"whatsapp_to_missing\");\n const url = `https://graph.facebook.com/v22.0/${encodeURIComponent(WHATSAPP_PHONE_NUMBER_ID)}/messages`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${WHATSAPP_TOKEN}` },\n body: JSON.stringify({ messaging_product: \"whatsapp\", to, type: \"text\", text: { body: text } })\n });\n if (!res.ok) throw new Error(`whatsapp_http_${res.status}`);\n return { provider: \"whatsapp\", status: res.status };\n}\n\nasync function deliverViaWebhook(channel, text) {\n const endpoint = String(channel.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"webhook_endpoint_missing\");\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text, platform: channel.platform || \"webhook\", ts: Date.now() })\n });\n if (!res.ok) throw new Error(`webhook_http_${res.status}`);\n return { provider: \"webhook\", status: res.status };\n}\n\nasync function deliverSocialMessage(channel, text) {\n const platform = String(channel.platform || \"webhook\").toLowerCase();\n if (platform === \"telegram\") return deliverViaTelegram(channel, text);\n if (platform === \"whatsapp\") return deliverViaWhatsapp(channel, text);\n return deliverViaWebhook(channel, text);\n}\n\nasync function deliverSocialWithRetry(channel, text) {\n let attempt = 0;\n let lastError = null;\n while (attempt <= SOCIAL_RETRY_MAX) {\n try {\n const result = await deliverSocialMessage(channel, text);\n return { ok: true, attempt: attempt + 1, ...result };\n } catch (error) {\n lastError = error;\n if (attempt >= SOCIAL_RETRY_MAX) break;\n const jitter = Math.floor(Math.random() * 90);\n await sleep(SOCIAL_RETRY_BASE_MS * Math.pow(2, attempt) + jitter);\n }\n attempt += 1;\n }\n return { ok: false, attempt: attempt + 1, error: String(lastError?.message || \"delivery_failed\") };\n}\n\nasync function dispatchCommandToAdapter(device, command) {\n const adapter = String(command?.adapter || device?.adapter || \"websocket\").toLowerCase();\n const endpoint = String(device?.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"device_endpoint_missing\");\n if (adapter === \"mqtt\") {\n // MQTT delivery is proxied via a bridge endpoint (HTTP webhook style).\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n type: \"mqtt-command\",\n topic: `castle/devices/${device.id}/commands`,\n action: command.action,\n params: command.params || {}\n })\n });\n if (!res.ok) throw new Error(`mqtt_bridge_http_${res.status}`);\n return { ok: true, adapter, via: \"http-bridge\", status: res.status };\n }\n if (!endpoint.startsWith(\"ws://\") && !endpoint.startsWith(\"wss://\")) throw new Error(\"invalid_websocket_endpoint\");\n const sendResult = await new Promise((resolve, reject) => {\n const ws = new WebSocket(endpoint);\n const timer = setTimeout(() => {\n try {\n ws.close();\n } catch {\n /* noop */\n }\n reject(new Error(\"websocket_timeout\"));\n }, 3000);\n ws.on(\"open\", () => {\n ws.send(\n JSON.stringify({\n type: \"castle-command\",\n commandId: command.id,\n deviceId: command.deviceId,\n action: command.action,\n params: command.params || {}\n }),\n (err) => {\n clearTimeout(timer);\n ws.close();\n if (err) reject(new Error(\"websocket_send_failed\"));\n else resolve({ ok: true, adapter: \"websocket\", via: \"ws\" });\n }\n );\n });\n ws.on(\"error\", () => {\n clearTimeout(timer);\n reject(new Error(\"websocket_connect_failed\"));\n });\n });\n return sendResult;\n}\n\nfunction readHttpJson(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => {\n if (!buf) return resolve({});\n const payload = safeJsonParse(buf);\n if (!payload || typeof payload !== \"object\") return reject(new Error(\"invalid_json_payload\"));\n resolve(payload);\n });\n req.on(\"error\", reject);\n });\n}\n\n/** Ham gövde (HMAC doğrulaması için). */\nfunction readHttpBodyText(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => resolve(buf));\n req.on(\"error\", reject);\n });\n}\n\nfunction maskOpaqueId(value) {\n const t = String(value || \"\");\n if (!t) return \"(empty)\";\n if (t.length <= 8) return \"****\";\n return `${t.slice(0, 4)}…${t.slice(-2)}`;\n}\n\nfunction readGatewayHttpToken(req) {\n const x = String(req.headers?.[\"x-castle-gateway-token\"] || \"\").trim();\n if (x) return x;\n const auth = String(req.headers?.authorization || \"\");\n const m = /^Bearer\\s+(.+)$/i.exec(auth);\n return m ? String(m[1] || \"\").trim() : \"\";\n}\n\nasync function resolveHttpUser(req) {\n const authResult = await verifyClientToken(req);\n if (authResult?.ok && authResult.user?.uid) {\n return { ok: true, uid: String(authResult.user.uid), mode: authResult.kind || \"auth\" };\n }\n if (ALLOW_DEV_ANON && ALLOW_DEV_HTTP_UID) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) return { ok: true, uid: `dev-${devUid.slice(0, 64)}`, mode: \"dev-header\" };\n }\n return { ok: false, reason: authResult?.reason || \"auth_required\" };\n}\n\n/**\n * Epistemic ledger ingest — Firebase uid OR valid transport token + X-Castle-Dev-Uid.\n * Prod disables CASTLE_ALLOW_DEV_HTTP_UID; LLM may still work while telemetry 401'd without this.\n */\nasync function resolveEpistemicIngestActor(req) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok && tok === REQUIRED_GATEWAY_TOKEN) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) {\n return { ok: true, uid: `gw-telemetry-${devUid.slice(0, 64)}`, mode: \"gateway-token-dev-uid\" };\n }\n return { ok: false, reason: \"dev_uid_required\" };\n }\n if (tok) {\n return { ok: false, reason: \"gateway_token_mismatch\" };\n }\n }\n const auth = await resolveHttpUser(req);\n if (auth.ok) return auth;\n return { ok: false, reason: auth.reason || \"auth_required\" };\n}\n\n/** Mesh actor: Firebase uid, dev header, or `guest:<uuid>` via header / `guestId` query (SSE cannot set headers). */\nasync function resolveMeshActor(req) {\n const auth = await resolveHttpUser(req);\n if (auth.ok) return { ok: true, clientUid: auth.uid, mode: auth.mode };\n if (ALLOW_MESH_GUEST) {\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const gid = String(req.headers?.[\"x-castle-guest-id\"] || q.searchParams.get(\"guestId\") || \"\").trim();\n if (gid) {\n const normalized = gid.startsWith(\"guest:\") ? gid.slice(0, 180) : `guest:${gid.slice(0, 160)}`;\n return { ok: true, clientUid: normalized, mode: \"guest-header\" };\n }\n }\n return { ok: false, reason: \"mesh_auth_required\" };\n}\n\n/** Live genesis runtime JSON (GET /rhizoh/genesis/runtime + SSE infra sampler). */\nasync function buildGenesisRuntimeSurfacePayloadLive() {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n let spiralWsActive = 0;\n if (spiralWssForGenesis) {\n for (const c of spiralWssForGenesis.clients) {\n if (c.readyState === 1) spiralWsActive += 1;\n }\n }\n const base = buildGenesisRuntimeSurfacePayload({\n infraMetrics,\n scoredHealth: scored,\n rhizohEnterpriseMetrics,\n mesh: meshContinuityAggregate(),\n workerHealth: worker,\n port: PORT,\n spiralWebSocketClientsActive: spiralWsActive\n });\n return {\n ...base,\n replayFingerprint: computeGenesisReplayFingerprintV0(base),\n gatewayCapabilities: buildGenesisGatewayCapabilitiesV0(),\n genesisStream: {\n schema: \"castle.genesis.stream_cursor.v0\",\n lastAcceptedSeq: getGenesisContinuitySeq(),\n clientIngress: {\n schema: GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n contract: \"castle.world_observation.ingress_envelope.v1\",\n postPath: rhizohRuntime.routes.genesisIngress,\n closure: getGenesisWorldObservationIngressStatusV0()\n },\n seqContinuity: getGenesisContinuitySeqAuditV0(),\n eventArchive: genesisContinuityEventArchiveEnabled()\n ? {\n enabled: true,\n projection: GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION,\n queryPath: rhizohRuntime.routes.genesisContinuityEvents\n }\n : { enabled: false },\n replayRouter: {\n schema: GENESIS_REPLAY_ROUTER_SCHEMA,\n queryPath: rhizohRuntime.routes.genesisReplay,\n queryHint: \"from=&to= or range=from-to; optional type=&checkpoints=0\",\n temporalDiffPath: rhizohRuntime.routes.genesisReplayDiff,\n temporalDiffSchema: GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA,\n equivalencePath: rhizohRuntime.routes.genesisReplayEquivalence,\n analyticsPath: rhizohRuntime.routes.genesisReplayAnalytics,\n evolutionPath: rhizohRuntime.routes.genesisReplayEvolution\n }\n }\n };\n}\n\nconst httpServer = createServer(async (req, res) => {\n const corsAllow = applyHttpCorsHeaders(req, res);\n if (req.method === \"OPTIONS\") {\n if (!corsAllow && req.headers?.origin) {\n console.warn(\"[GATEWAY_CORS] preflight rejected for origin:\", req.headers.origin);\n }\n res.writeHead(corsAllow ? 204 : 403);\n res.end();\n return;\n }\n\n const pathname = getHttpPathname(req);\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/genesis/__ping\") {\n sendJson(res, 200, {\n ok: true,\n pid: process.pid,\n entry: __filename,\n time: Date.now(),\n genesis: true\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/epistemic/__probe\") {\n sendJson(res, 200, {\n ok: true,\n epistemicIngestActorV2: true,\n routes: {\n seal: rhizohRuntime.routes.epistemicSeal,\n logsBatch: rhizohRuntime.routes.epistemicLogsBatch,\n authorityLedgerBatch: rhizohRuntime.routes.authorityLedgerBatch,\n authorityLedgerReplay: rhizohRuntime.routes.authorityLedgerReplay,\n authorityEpochMerge: rhizohRuntime.routes.authorityEpochMerge\n },\n hasGatewayToken: Boolean(REQUIRED_GATEWAY_TOKEN),\n gatewayTokenLen: REQUIRED_GATEWAY_TOKEN ? REQUIRED_GATEWAY_TOKEN.length : 0\n });\n return;\n }\n\n if (\n req.method === \"GET\" &&\n (pathname === \"/rhizoh/live/world-feed\" ||\n pathname === \"/rhizoh/live/sports-bundle\" ||\n pathname === \"/rhizoh/live/news-headlines\")\n ) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_live_world_feed:${ip}`, RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const out =\n pathname === \"/rhizoh/live/sports-bundle\"\n ? await handleLiveSportsBundleGetV0()\n : pathname === \"/rhizoh/live/news-headlines\"\n ? await handleLiveNewsHeadlinesGetV0(req)\n : await handleLiveWorldFeedGetV0(req);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/webhooks/stripe\") {\n await handleStripeMembershipWebhook(req, res);\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/billing/stripe/checkout\") {\n await handleStripeCheckoutCreate(req, res, { readHttpJson, resolveHttpUser, sendJson });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/live\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, live: false, dns: true, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n sendJson(res, 200, { ok: true, live: true, dns: true, service: \"castle-gateway\", ts: Date.now() });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/cors\") {\n const reqOrigin = normalizeHttpOrigin(req.headers?.origin);\n const allowOrigin = accessControlAllowOriginValue(req);\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n requestOrigin: reqOrigin || null,\n allowed: Boolean(allowOrigin),\n allowOrigin: allowOrigin || null,\n allowSetSize: httpCors.allowSet.size\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/ready\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, ready: false, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n try {\n const persistence = getFirebasePersistence();\n const llmProbe = rhizohLlmEnvConfigured();\n sendJson(res, 200, {\n ok: true,\n ready: true,\n dns: true,\n persistence: persistence.mode,\n service: \"castle-gateway\",\n gateway: \"ok\",\n llm: llmProbe ? \"ok\" : \"degraded\",\n spine: llmProbe ? \"ok\" : \"partial\"\n });\n } catch (e) {\n sendJson(res, 503, { ok: false, ready: false, reason: String(e?.message || e), service: \"castle-gateway\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/reality\") {\n const payload = buildRealityHealthPayload();\n sendJson(res, payload.ok ? 200 : 503, payload);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/substrate\") {\n const authority = buildGatewaySubstrateAuthoritySnapshotV0();\n const operational = buildSubstrateOperationalSnapshotV0();\n const drift = buildRealityHealthPayload();\n const ok = Boolean(authority.productionAuthoritySatisfied) && drift.ok;\n sendJson(res, ok ? 200 : 503, {\n ok,\n service: \"castle-gateway\",\n ts: Date.now(),\n authority,\n operational,\n drift: { ok: drift.ok, errors: drift.drift?.errors, warnings: drift.drift?.warnings }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.councilAnomalyReasoning) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_council_anomaly:${ip}`, 30, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const body = await readHttpJson(req);\n const out = handleCouncilAnomalyReasoningPostV0(body);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/substrate/health\") {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const body = await readHttpJson(req);\n const uid = (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestClientSubstrateHealthV0(body, { uid, clientId: uid });\n if (!result.ok) {\n sendJson(res, 400, result);\n return;\n }\n recordSubstrateOtelEventV0(\"client_health_ingest\", { uid: String(uid).slice(0, 32) });\n sendJson(res, 200, result);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/deps\") {\n const t0 = Date.now();\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, {\n ok: false,\n dns: true,\n llm: false,\n firestore: false,\n phase: \"maintenance\",\n latencyMs: Date.now() - t0,\n service: \"castle-gateway\"\n });\n return;\n }\n const persistence = getFirebasePersistence();\n let firestoreOk = persistence.mode !== \"firebase\";\n let firestoreDetail = persistence.mode === \"file\" ? \"file_mode\" : \"not_applicable\";\n if (persistence.mode === \"firebase\" && persistence.db) {\n const pr = await probeFirestoreReachable(persistence.db, 2000);\n firestoreOk = pr.ok;\n firestoreDetail = pr.reason || (pr.ok ? \"reachable\" : \"fail\");\n } else if (persistence.mode === \"firebase\" && !persistence.db) {\n firestoreOk = false;\n firestoreDetail = \"firebase_not_initialized\";\n }\n const llmOk = rhizohLlmEnvConfigured();\n const overallOk = llmOk && firestoreOk;\n const latencyMs = Date.now() - t0;\n sendJson(res, 200, {\n ok: overallOk,\n dns: true,\n llm: llmOk,\n firestore: persistence.mode === \"firebase\" ? firestoreOk : false,\n persistence: persistence.mode,\n firestoreDetail,\n latencyMs,\n service: \"castle-gateway\",\n wsPort: PORT\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health\") {\n const persistence = getFirebasePersistence();\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n wsPort: PORT,\n persistence: persistence.mode,\n presenceMesh: \"sse\"\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/health\") {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n sendJson(res, 200, {\n ok: true,\n role: \"gateway\",\n queueLagMs: infraMetrics.queueLag,\n queueDepth: infraMetrics.queueDepth,\n errors: infraMetrics.errors,\n status: scored.status,\n score: scored.score,\n reasons: scored.reasons,\n worker: worker\n ? {\n status: worker.status,\n score: worker.score,\n reasons: worker.reasons\n }\n : null,\n timestamp: Date.now()\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisRuntime) {\n const payload = await buildGenesisRuntimeSurfacePayloadLive();\n sendJson(res, 200, payload);\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.genesisIngress) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const actor = await resolveMeshActor(req);\n const clientId = actor.ok ? actor.clientUid : (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestGenesisWorldObservationV0(body, { clientId });\n if (!result.ok) {\n const status =\n result.error === \"rate_limited\" || result.error === \"agent_spoke_throttled\"\n ? 429\n : result.error === \"payload_too_large\"\n ? 413\n : 400;\n sendJson(res, status, result);\n return;\n }\n sendJson(res, result.idempotent ? 200 : 202, result);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisStream) {\n const corsOrigin = accessControlAllowOriginValue(req);\n if (!corsOrigin && String(process.env.NODE_ENV || \"\").toLowerCase() === \"production\") {\n const og = String(req.headers?.origin || \"\").trim();\n if (og) {\n console.warn(\"[GATEWAY] genesis SSE: missing Access-Control-Allow-Origin for browser Origin (CORS whitelist mismatch):\", og);\n }\n }\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n res.write(\"retry: 3000\\n\\n\");\n res.write(\": genesis continuity stream\\n\\n\");\n res.write(\"event: genesis\\n\");\n res.write(`data: {\"booted\":true,\"pid\":${process.pid}}\\n\\n`);\n registerGenesisSseClient(res, req);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLatest) {\n const cp = getLatestGenesisSignedCheckpoint();\n if (!cp) {\n sendJson(res, 404, { ok: false, error: \"no_checkpoint_yet\", hint: \"wait_for_seq_interval_or_configure_signing_secret\" });\n return;\n }\n sendJson(res, 200, { ok: true, checkpoint: cp });\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(rhizohRuntime.routes.genesisCheckpointBySeqPrefix)) {\n const tail = pathname.slice(rhizohRuntime.routes.genesisCheckpointBySeqPrefix.length).split(\"/\")[0];\n const seq = parseInt(String(tail || \"\").trim(), 10);\n const out = await genesisCheckpointQueryBySeqV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"checkpoint_not_found\") {\n sendJson(res, 404, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointRange) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryRangeV0(from, to);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"range_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLineage) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const seq = parseInt(String(u.searchParams.get(\"seq\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryLineageV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"lineage_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisContinuityEvents) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const limit = parseInt(String(u.searchParams.get(\"limit\") || \"\").trim(), 10);\n const out = await queryGenesisContinuityEventArchiveV0(from, to, type, limit);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\" || out.error === \"event_archive_disabled\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"archive_file_too_large\") {\n sendJson(res, 413, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplay) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await resolveGenesisReplayRouterV1({\n from: fromSeq,\n to: toSeq,\n type,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayDiff) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const out = await computeGenesisReplayTemporalDiffV1({ from: fromSeq, to: toSeq, type });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEquivalence) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from1 = parseInt(String(u.searchParams.get(\"from1\") || \"\").trim(), 10);\n const to1 = parseInt(String(u.searchParams.get(\"to1\") || \"\").trim(), 10);\n const from2 = parseInt(String(u.searchParams.get(\"from2\") || \"\").trim(), 10);\n const to2 = parseInt(String(u.searchParams.get(\"to2\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await compareGenesisReplayEquivalenceV1({\n from1,\n to1,\n from2,\n to2,\n type,\n includeCheckpoints\n });\n if (\n !out.ok &&\n (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")\n ) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayAnalytics) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayAnalyticsV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEvolution) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const collapseWindows = parseInt(String(u.searchParams.get(\"collapseWindows\") || \"0\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayEvolutionV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n collapseWindows,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/metrics\") {\n res.writeHead(200, { \"Content-Type\": \"text/plain; version=0.0.4; charset=utf-8\" });\n res.end(renderInfraMetricsProm());\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.infraPrometheusMetrics) {\n if (process.env.CASTLE_PROMETHEUS_METRICS !== \"1\") {\n sendJson(res, 404, { ok: false, error: \"prometheus_metrics_disabled\" });\n return;\n }\n res.writeHead(200, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(renderRhizohPrometheusMetrics());\n return;\n }\n if (req.method === \"GET\" && pathname.startsWith(\"/infra/traces/\")) {\n const traceId = decodeURIComponent(pathname.slice(\"/infra/traces/\".length));\n const item = await getTraceById(traceId);\n if (!item) return sendJson(res, 404, { ok: false, error: \"trace_not_found\" });\n return sendJson(res, 200, { ok: true, trace: item });\n }\n if (req.method === \"GET\" && pathname === \"/infra/traces\") {\n const u = new URL(String(req.url || \"/infra/traces\"), `http://${req.headers.host || \"localhost\"}`);\n const sessionId = String(u.searchParams.get(\"sessionId\") || \"\").trim();\n const items = sessionId ? await listTracesBySession(sessionId) : [];\n return sendJson(res, 200, { ok: true, sessionId, traces: items });\n }\n\n /** Presence room mesh (C): JOIN / LEAVE / SNAPSHOT / DELTA / REPLAY (HTTP); SUBSCRIBE = SSE stream. */\n if (req.method === \"POST\" && pathname === \"/presence/mesh/join\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid, clientUid: actor.clientUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/leave\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshLeave(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/snapshot\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n sendJson(res, 200, meshSnapshot(roomUid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/delta\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, MAX_MESSAGE_BYTES);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const seq = meshAppendDelta(roomUid, actor.clientUid, {\n node: body?.node,\n projectionPatch: body?.projectionPatch,\n writerSubject: body?.writerSubject\n });\n sendJson(res, 200, { ok: true, roomUid, seq });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/replay\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const fromNodeId = body?.fromNodeId != null ? String(body.fromNodeId) : \"\";\n const fromSeq = body?.fromSeq;\n if (fromNodeId) {\n sendJson(res, 200, meshReplayFromNodeId(roomUid, fromNodeId));\n return;\n }\n sendJson(res, 200, meshReplayFromSeq(roomUid, fromSeq));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/presence/mesh/subscribe\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const roomUid = String(q.searchParams.get(\"roomUid\") || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n const write = (chunk) => {\n try {\n res.write(chunk);\n } catch {\n /* closed */\n }\n };\n const unsub = meshSubscribeSse(roomUid, write);\n const onClose = () => {\n unsub();\n meshLeave(roomUid, actor.clientUid);\n try {\n res.end();\n } catch {\n /* ignore */\n }\n };\n req.on(\"close\", onClose);\n req.on(\"aborted\", onClose);\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, uid: auth.uid, mode: auth.mode, items: await listConnections(auth.uid) });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const id = await createConnection(auth.uid, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"create_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n await updateConnection(auth.uid, id, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"update_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"DELETE\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n await deleteConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"delete_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url?.startsWith(\"/rhizoh/network/presence\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const u = new URL(req.url, \"http://localhost\");\n const roomKey = String(u.searchParams.get(\"room\") || \"world_space_c2c_v0\").slice(0, 64);\n const sessionId = String(u.searchParams.get(\"sessionId\") || \"\").trim();\n const unified = listUnifiedGatewayPresenceV0({ roomKey, sessionId: sessionId || undefined });\n sendJson(res, 200, {\n ok: true,\n roomKey,\n sessionId: sessionId || null,\n presence: unified,\n castlePeers: unified.castlePeers,\n matchMembers: unified.matchMembers,\n services: unified.services\n });\n } catch (error) {\n sendJson(res, 500, { ok: false, error: error?.message || \"presence_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/rhizoh/sync/vault\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const snapshot = await getCloudSyncSnapshotV0(auth.uid);\n sendJson(res, 200, { ok: true, uid: auth.uid, snapshot });\n } catch (error) {\n sendJson(res, 500, { ok: false, error: error?.message || \"cloud_sync_pull_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/rhizoh/sync/vault\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const snapshot = await mergeCloudSyncSnapshotV0(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, uid: auth.uid, snapshot });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"cloud_sync_push_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections/test\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n if (!checkHttpRateLimit(`llm_conn_test:${auth.uid}`, RL_LLM_CONN_TEST_PER_MIN, 60_000)) {\n return sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n }\n const hasBodyKey = !!String(payload?.apiKey || \"\").trim();\n const result = await queryRhizohLlm(\n {\n message: String(payload?.message || \"Ping from Castle Gateway\"),\n context: payload?.context || { source: \"connection_test\" },\n provider: payload?.provider,\n model: payload?.model,\n apiKey: payload?.apiKey || \"\"\n },\n { allowExternalApiKey: true, llmKeySource: hasBodyKey ? \"auto\" : \"env\" }\n );\n logLlmAccess({\n route: \"/llm/connections/test\",\n uid: auth.uid,\n llmKeyBillingOwner: result.llmKeyBillingOwner,\n llmKeyOrigin: result.llmKeyOrigin,\n provider: result.provider,\n model: result.model,\n connectionId: null\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"connection_test_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAgentIdentities(auth.uid);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerAgentIdentity({\n ownerUid: auth.uid,\n agentId: payload?.agentId,\n role: payload?.role,\n personaSeed: payload?.personaSeed || {},\n capabilityLevel: payload?.capabilityLevel\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/agents/identities/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const owns = await getAgentIdentity(id);\n if (!owns || owns.ownerUid !== auth.uid) return sendJson(res, 403, { ok: false, error: \"forbidden\" });\n const payload = await readHttpJson(req);\n const row = await updateAgentIdentity(id, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/academy/events\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/academy/events\"}`);\n const status = String(url.searchParams.get(\"status\") || \"\");\n const items = await listAcademyEvents({ ownerUid: auth.uid, status, limit: 120 });\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/academy/events\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const ev = await queueAcademyEvent({\n type: payload?.type,\n roomId: payload?.roomId,\n ownerUid: auth.uid,\n participants: payload?.participants || [],\n topic: payload?.topic || \"academy\",\n payload: payload?.payload || {}\n });\n sendJson(res, 200, { ok: true, event: ev });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && String(req.url || \"\").startsWith(\"/academy/events/\") && String(req.url || \"\").endsWith(\"/resolve\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const parts = String(req.url || \"\").split(\"/\").filter(Boolean);\n const eventId = parts[2] || \"\";\n try {\n const payload = await readHttpJson(req);\n const out = await resolveAcademyEvent(eventId, payload || {});\n sendJson(res, 200, { ok: true, event: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_resolve_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/memory\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/memory\"}`);\n const scope = url.searchParams.get(\"scope\") === \"agents\" ? \"agents\" : \"users\";\n const id = String(url.searchParams.get(\"id\") || (scope === \"agents\" ? \"\" : auth.uid));\n const kind = String(url.searchParams.get(\"kind\") || \"\");\n const limit = Number(url.searchParams.get(\"limit\") || 40);\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n sendJson(res, 200, { ok: true, scope, id, kind, items: await listMemories({ scope, id, kind, limit }) });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const profile = await getPersonaGoalMemory(auth.uid);\n sendJson(res, 200, { ok: true, profile });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const profile = await setPersonaGoalMemory({\n uid: auth.uid,\n personaPatch: payload?.personaPatch || {},\n goals: Array.isArray(payload?.goals) ? payload.goals : null,\n preferencesPatch: payload?.preferencesPatch || {}\n });\n sendJson(res, 200, { ok: true, profile });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"profile_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/compact\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const out = await autoCompactMemories({ scope, id });\n sendJson(res, 200, { ok: true, scope, id, ...out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_compact_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/context\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const context = await getMemoryContext({\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n query: String(payload?.query || \"\"),\n limit: Number(payload?.limit || 80)\n });\n sendJson(res, 200, { ok: true, context });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_context_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/place-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const placeName = String(payload?.placeName || \"\").trim();\n if (!placeName) return sendJson(res, 400, { ok: false, error: \"placeName_required\" });\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const persona = String(payload?.waypointPersona || payload?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(placeName)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Place: ${placeName}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Give a concise conversational briefing about this place for a walking-route companion.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-place-brief\", placeName, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `PLACE_BRIEF:${placeName} => ${String(summary.reply || \"\").slice(0, 900)}`,\n tags: [\"place\", \"brief\", \"event-layer\"],\n importance: 0.62,\n meta: { placeName }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"place-brief\",\n text: `${placeName} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: {\n placeName,\n directive: summary.directive,\n lat: hit?.lat ? Number(hit.lat) : null,\n lon: hit?.lon ? Number(hit.lon) : null\n }\n });\n sendJson(res, 200, { ok: true, place: hit || null, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"place_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/route-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const waypoints = Array.isArray(payload?.waypoints) ? payload.waypoints.slice(0, 24) : [];\n const out = [];\n for (const wp of waypoints) {\n const name = String(wp?.name || \"\").trim();\n if (!name) continue;\n const persona = String(wp?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(name)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Waypoint: ${name}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Generate a concise spoken briefing for route waypoint.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-route-brief\", waypoint: name, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n out.push({\n name,\n persona,\n place: hit || null,\n reply: summary.reply,\n directive: summary.directive\n });\n }\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `ROUTE_BRIEF: waypoints=${out.length} ${out.map((x) => `${x.name}:${x.persona}`).join(\" | \")}`.slice(0, 1300),\n tags: [\"route\", \"waypoint\", \"brief\", \"event-layer\"],\n importance: 0.74,\n meta: { count: out.length }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"route-brief\",\n text: out.map((x) => `${x.name}:${String(x.reply || \"\").slice(0, 140)}`).join(\" | \").slice(0, 1400),\n roomId: \"studio-main\",\n meta: {\n waypointCount: out.length,\n waypoints: out.map((x) => ({\n name: x.name,\n persona: x.persona,\n lat: x.place?.lat ? Number(x.place.lat) : null,\n lon: x.place?.lon ? Number(x.place.lon) : null\n }))\n }\n });\n sendJson(res, 200, { ok: true, items: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"route_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/pdf-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const title = String(payload?.title || \"document\");\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const providedText = String(payload?.text || \"\");\n const url = String(payload?.url || \"\");\n let content = providedText;\n if (!content && url) content = await fetchTextFromUrl(url);\n if (!content) return sendJson(res, 400, { ok: false, error: \"text_or_supported_url_required\" });\n const summary = await queryRhizohLlm(\n {\n message: `Summarize this document and provide key points + practical actions.\\nTitle:${title}\\nContent:\\n${content.slice(0, 12000)}`,\n context: { source: \"event-layer-pdf-brief\", title },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `DOC_BRIEF:${title} => ${String(summary.reply || \"\").slice(0, 1000)}`,\n tags: [\"document\", \"brief\", \"library\", \"event-layer\"],\n importance: 0.7,\n meta: { title, url: url || null }\n });\n await addTranscript(auth.uid, {\n source: \"library-brief\",\n eventType: \"pdf-brief\",\n text: `${title} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { title }\n });\n sendJson(res, 200, { ok: true, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/pdf/upload\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req, 8 * 1024 * 1024);\n const job = await enqueuePdfJob({\n uid: auth.uid,\n fileName: payload?.fileName || \"document.pdf\",\n bytesBase64: payload?.contentBase64\n });\n sendJson(res, 200, { ok: true, job });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_upload_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/session\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createPublishSession(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session\",\n text: `Publish session armed: ${row.protocol} ${row.target}`.slice(0, 500),\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: row.id, bridge: row.bridge }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/studio/publish/sessions\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listPublishSessions(auth.uid, 80);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/health-check\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const protocol = String(payload?.protocol || \"WHIP\");\n const target = String(payload?.target || \"\");\n const health = await checkPublishTargetHealth(protocol, target);\n if (payload?.sessionId) {\n await updatePublishSession(auth.uid, String(payload.sessionId), {\n status: health.ok ? \"healthy\" : \"degraded\",\n reconnectPolicy: {\n enabled: true,\n backoffSec: [2, 5, 10, 20],\n nextRetryInSec: health.reconnectInSec || 0,\n maxRetries: 8,\n reason: health.reason\n }\n });\n }\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-health\",\n text: `${protocol} ${target} => ${health.ok ? \"healthy\" : \"degraded\"} (${health.reason})`,\n roomId: \"studio-main\",\n meta: { protocol, target, reconnect: !!health.shouldReconnect }\n });\n sendJson(res, 200, { ok: true, health });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_health_check_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/studio/publish/sessions/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updatePublishSession(auth.uid, id, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session-update\",\n text: `Session ${id} -> ${row.status || \"updated\"}`,\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: id, status: row.status, ingestUrl: row.ingestUrl || \"\" }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/transcripts\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/transcripts\"}`);\n const limit = Math.max(1, Math.min(200, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await listTranscripts(auth.uid, limit);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/transcripts\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await addTranscript(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"transcript_write_failed\" });\n }\n return;\n }\n\n /** E1: broadcast intent → GreenRoom capability → transcript + memory artifact */\n if (req.method === \"POST\" && req.url === \"/studio/capabilities/greenroom\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const traceId = String(payload?.traceId || `TRC-${Date.now().toString(36).toUpperCase()}`);\n const title = String(payload?.title || \"Castle Live Broadcast\").slice(0, 200);\n const intentRaw = String(payload?.intentRaw || \"\").slice(0, 800);\n const audienceEstimate = Math.max(0, Math.min(50_000_000, Number(payload?.audienceEstimate) || 1200));\n const roomId = String(payload?.roomId || \"greenroom-main\");\n const transcriptRow = await addTranscript(auth.uid, {\n source: \"rhizoh\",\n eventType: \"BROADCAST_ROUTED\",\n text: `GreenRoom · ${title}${intentRaw ? `\\nIntent: ${intentRaw}` : \"\"}`,\n roomId,\n meta: {\n ack: \"BROADCAST_ROUTED\",\n capability: \"greenroom\",\n intentType: \"broadcast\",\n traceId,\n audienceEstimate,\n artifact: {\n kind: \"broadcast_card\",\n title,\n traceId,\n roomId,\n createdAt: Date.now()\n }\n }\n });\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n text: `GREENROOM_BROADCAST:${title} trace=${traceId}`,\n tags: [\"broadcast\", \"greenroom\", \"artifact\", \"castle-library\"],\n importance: 0.72,\n kind: \"episodic\",\n meta: { traceId, capability: \"greenroom\", transcriptId: transcriptRow.id }\n });\n sendJson(res, 200, {\n ok: true,\n ack: \"BROADCAST_ROUTED\",\n traceId,\n transcript: transcriptRow,\n replayPath: `/replay/${encodeURIComponent(traceId)}`,\n sharePath: `/replay/${encodeURIComponent(traceId)}`\n });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"greenroom_capability_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/metadata/search\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/metadata/search\"}`);\n const q = String(url.searchParams.get(\"q\") || \"\");\n const limit = Math.max(1, Math.min(300, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await searchMetadata(auth.uid, q, limit);\n sendJson(res, 200, { ok: true, q, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerDevice(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, device: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"device_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const lat = Number(payload?.lat);\n const lon = Number(payload?.lon);\n const speed = Math.max(0, Number(payload?.speed || 0));\n const cfgCenterLat = Number(process.env.CASTLE_GEOFENCE_CENTER_LAT || 41.0082);\n const cfgCenterLon = Number(process.env.CASTLE_GEOFENCE_CENTER_LON || 28.9784);\n const cfgRadiusM = Math.max(100, Number(process.env.CASTLE_GEOFENCE_RADIUS_M || 55000));\n const cfgSpeedCap = Math.max(1, Number(process.env.CASTLE_SPEED_CAP_MPS || 35));\n const dist = Number.isFinite(lat) && Number.isFinite(lon) ? haversineMeters(cfgCenterLat, cfgCenterLon, lat, lon) : 0;\n const geofenceOk = !Number.isFinite(dist) ? true : dist <= cfgRadiusM;\n const speedOk = speed <= cfgSpeedCap;\n const failsafe = !(geofenceOk && speedOk);\n const safety = {\n geofenceOk,\n speedOk,\n failsafe,\n distanceFromCenterM: Number.isFinite(dist) ? dist : null,\n speedCapMps: cfgSpeedCap\n };\n const row = await appendTelemetry(auth.uid, deviceId, {\n lat,\n lon,\n speed,\n battery: payload?.battery,\n mode: payload?.mode || (failsafe ? \"FAILSAFE_HOLD\" : \"ACTIVE\"),\n raw: payload?.raw || {}\n });\n await updateDevice(auth.uid, deviceId, {\n status: failsafe ? \"failsafe\" : \"active\",\n lastTelemetryAt: row.ts,\n safetyState: safety\n });\n if (failsafe) {\n await addTranscript(auth.uid, {\n source: \"robotics-guardrails\",\n eventType: \"failsafe\",\n text: `Device ${deviceId} failsafe triggered geofenceOk=${geofenceOk} speedOk=${speedOk}`,\n roomId: \"studio-main\",\n meta: { deviceId, safety }\n });\n }\n sendJson(res, 200, { ok: true, telemetry: row, safety, command: failsafe ? \"FAILSAFE_HOLD\" : \"CONTINUE\" });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"telemetry_ingest_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listTelemetry(auth.uid, 150);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createAutomation(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"automation-scheduled\",\n text: `Automation scheduled: ${row.title} at ${new Date(row.scheduleAt).toLocaleString()}`,\n roomId: \"studio-main\",\n meta: { automationId: row.id, action: row.action, deviceId: row.deviceId }\n });\n sendJson(res, 200, { ok: true, automation: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"automation_create_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAutomations(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listDevices(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const devs = await listDevices(auth.uid, 500);\n const d = devs.find((x) => x.id === deviceId);\n if (!d) return sendJson(res, 404, { ok: false, error: \"device_not_found\" });\n const action = String(payload?.action || \"\");\n const caps = Array.isArray(d.capabilityProfile) ? d.capabilityProfile : [];\n if (action && caps.length && !caps.includes(action) && !caps.includes(\"*\")) {\n return sendJson(res, 400, { ok: false, error: \"unsupported_action_for_device\", capabilityProfile: caps });\n }\n const cmd = await enqueueCommand(auth.uid, {\n deviceId,\n action: action || \"NOOP\",\n params: payload?.params || {},\n adapter: payload?.adapter || d.adapter || \"websocket\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-command-queue\",\n eventType: \"command-queued\",\n text: `${cmd.deviceId} -> ${cmd.action}`,\n roomId: \"studio-main\",\n meta: { commandId: cmd.id, adapter: cmd.adapter }\n });\n sendJson(res, 200, { ok: true, command: cmd });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listCommands(auth.uid, 200);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/robotics/commands/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updateCommandStatus(auth.uid, id, payload?.status || \"sent\");\n sendJson(res, 200, { ok: true, command: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/rhizoh/bridge\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const result = await runRhizohBrainV2({\n message: String(payload?.message || \"robotics bridge command\"),\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n worldState: payload?.worldState || {},\n deviceState: payload?.deviceState || {},\n intent: \"robotics-mechanics\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-bridge\",\n eventType: \"robotics-command\",\n text: `Bridge -> ${String(result.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { directive: result.directive, agentId: payload?.agentId || \"\" }\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"robotics_bridge_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/planner/graph\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const graph = buildTaskActionGraph(String(payload?.task || \"\"), payload?.context || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"planner-graph\",\n text: `Planner graph built: ${graph.taskTitle} nodes=${graph.nodes.length}`,\n roomId: \"studio-main\",\n meta: { taskTitle: graph.taskTitle, nodeCount: graph.nodes.length }\n });\n sendJson(res, 200, { ok: true, graph });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"planner_graph_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerSocialChannel(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, channel: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_channel_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listSocialChannels(auth.uid, 100);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/broadcast\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const text = String(payload?.text || \"\").slice(0, 1200);\n const channels = await listSocialChannels(auth.uid, 100);\n const enabled = channels.filter((c) => c.status === \"enabled\");\n const deliveries = [];\n for (const c of enabled) {\n const out = await deliverSocialWithRetry(c, text);\n deliveries.push({ id: c.id, platform: c.platform, ...out });\n }\n const okCount = deliveries.filter((d) => d.ok).length;\n await addTranscript(auth.uid, {\n source: \"social-layer\",\n eventType: \"social-broadcast\",\n text: `Broadcast delivered ${okCount}/${enabled.length} channel(s): ${text.slice(0, 240)}`,\n roomId: \"studio-main\",\n meta: { deliveries }\n });\n sendJson(res, 200, { ok: true, sentChannels: okCount, totalChannels: enabled.length, deliveries });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_broadcast_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/ethics/programs\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, ...buildEthicsPrograms() });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands/dispatch\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const all = await listCommands(auth.uid, 200);\n const queued = all.filter((c) => String(c.status || \"queued\") === \"queued\").slice(0, 50);\n const devices = await listDevices(auth.uid, 500);\n const byId = new Map(devices.map((d) => [d.id, d]));\n const results = [];\n for (const cmd of queued) {\n const dev = byId.get(cmd.deviceId);\n if (!dev) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: \"device_not_found\" });\n continue;\n }\n try {\n const dispatched = await dispatchCommandToAdapter(dev, cmd);\n await updateCommandStatus(auth.uid, cmd.id, \"sent\");\n results.push({ id: cmd.id, ok: true, adapter: dispatched.adapter, via: dispatched.via });\n } catch (error) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: String(error?.message || \"dispatch_failed\") });\n }\n }\n await addTranscript(auth.uid, {\n source: \"robotics-command-dispatch\",\n eventType: \"command-dispatch-batch\",\n text: `Dispatch batch size=${results.length} ok=${results.filter((x) => x.ok).length}`,\n roomId: \"studio-main\",\n meta: { results: results.slice(0, 30) }\n });\n sendJson(res, 200, { ok: true, processed: results.length, results });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"dispatch_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/pdf/jobs/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n const job = await getPdfJob(auth.uid, id);\n if (!job) return sendJson(res, 404, { ok: false, error: \"pdf_job_not_found\" });\n sendJson(res, 200, { ok: true, job });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const row = await appendMemory({\n scope,\n id,\n text: payload?.text,\n tags: payload?.tags || [],\n importance: payload?.importance,\n kind: payload?.kind || \"episodic\",\n meta: { uid: auth.uid, ...(payload?.meta || {}) }\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.externalTruth) {\n try {\n if (process.env.RHIZOH_EXTERNAL_TRUTH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_truth:${ip}`, RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n sendJson(res, 200, buildRhizohExternalGroundTruthPayload(req));\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.productOutcome) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const rawBody = await readHttpBodyText(req, 64 * 1024);\n const body = rawBody.trim() ? safeJsonParse(rawBody) : null;\n if (!body || typeof body !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"invalid_json_payload\" });\n return;\n }\n const sigHdr = String(req.headers?.[\"x-rhizoh-outcome-signature\"] || \"\").trim();\n const hmacV = verifyRhizohOutcomeHmac(rawBody, sigHdr);\n if (!hmacV.ok) {\n sendJson(res, 401, { ok: false, error: hmacV.error || \"outcome_hmac_invalid\" });\n return;\n }\n const srcTokHdr = String(req.headers?.[\"x-rhizoh-outcome-source-token\"] || \"\").trim();\n const srcV = verifyRhizohOutcomeSourceToken(body.source, srcTokHdr);\n if (!srcV.ok) {\n sendJson(res, 401, { ok: false, error: srcV.error || \"outcome_source_token_invalid\" });\n return;\n }\n const subjectKey = String(body.subjectKey || \"\").trim().slice(0, 128);\n if (\n subjectKey &&\n !checkHttpRateLimit(\n `rhizoh_product_outcome_subj:${subjectKey}`,\n RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN,\n 60_000\n )\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_subject\" });\n return;\n }\n const result = ingestRhizohProductOutcomeHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n schemaVersion: \"1.1.0\",\n aggregate: result.aggregate,\n integrity: result.integrity\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"outcome_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.productOutcomeAggregate) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome_agg:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const u = new URL(String(req.url || \"/\"), \"http://localhost\");\n const cohortId = u.searchParams.get(\"cohortId\") || \"\";\n const decisionFingerprint = u.searchParams.get(\"decisionFingerprint\") || \"\";\n const limit = u.searchParams.get(\"limit\") || \"50\";\n const rows = listRhizohProductOutcomeAggregates({\n cohortId: cohortId || undefined,\n decisionFingerprint: decisionFingerprint || undefined,\n limit: Number(limit) || 50\n });\n sendJson(res, 200, { ok: true, schemaVersion: \"1.1.0\", rows });\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.externalLossBatch) {\n try {\n if (process.env.RHIZOH_EXTERNAL_LOSS_BATCH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_loss_batch:${ip}`, RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const result = ingestRhizohExternalLossBatchHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, { ok: true, accepted: result.accepted ?? 0 });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"batch_failed\") });\n }\n return;\n }\n\n function moderationAdminOk(req) {\n const expected = String(process.env.CASTLE_MODERATION_ADMIN_KEY || \"\").trim();\n if (!expected) return false;\n const got = String(req.headers[\"x-castle-moderation-key\"] || \"\").trim();\n return got.length > 0 && got === expected;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.opsAbuseReport) {\n try {\n const body = await readHttpJson(req, 32 * 1024);\n const auth = await resolveHttpUser(req);\n const entry = submitAbuseReportV0({\n reporterUid: auth.ok ? auth.uid : null,\n targetUid: body?.targetUid ?? null,\n category: body?.category,\n detail: body?.detail,\n traceId: body?.traceId ?? null\n });\n sendJson(res, 202, { ok: true, reportId: entry.id, status: entry.status });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsModerationQueue) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n queue: listModerationQueueV0(100),\n softBlocked: listSoftBlockedUsersV0()\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-block\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const out = softBlockUserV0(body?.uid, body?.reason);\n sendJson(res, out.ok ? 200 : 400, out);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-unblock\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n sendJson(res, 200, removeSoftBlockV0(body?.uid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsAgentSnapshots) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n const limit = Number(new URL(req.url || \"\", \"http://local\").searchParams.get(\"limit\") || 50);\n sendJson(res, 200, { ok: true, snapshots: listRecentAgentSnapshotsV0(limit) });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.academicObservatoryExport) {\n const auth = await resolveHttpUser(req);\n const out = handleAcademicObservatoryExportGetV0(req, auth);\n sendJson(res, out.status, out.body);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsHardeningStatus) {\n const auth = await resolveHttpUser(req);\n const principal = auth.ok ? `uid:${auth.uid}` : null;\n const gclSnap = await getGlobalCostLedgerSnapshotV0(principal);\n const economicStrategy = evaluateEconomicStrategyL3V0(gclSnap);\n const hardeningTenantId = resolveHardeningTenantIdV0(auth, req);\n const unifiedState = await buildUnifiedStateNarrativeV0({\n tenantId: hardeningTenantId ?? undefined,\n platformScope: !hardeningTenantId,\n principal\n });\n sendJson(res, 200, {\n ok: true,\n schema: \"rhizoh.ops.hardening_status.v0\",\n containment: readAgentContainmentConfigV0(),\n cost: readCostContainmentConfigV0(),\n gcl: {\n backend: await getGclBackendStatusV0(),\n snapshot: gclSnap\n },\n economicStrategyL3: economicStrategy,\n phasedRollout: await getPhasedRolloutStatsV0(),\n tier: readPhasedRolloutTierV0(),\n principalCost: auth.ok ? await getCostContainmentStatsV0(principal) : null,\n stressTaxonomy: verifyStressTaxonomyCoverageV0(),\n stressConflictResolution: verifyStressConflictResolutionV0(),\n resolutionStabilityEnvelope: verifyResolutionStabilityEnvelopeV0(),\n unifiedState: {\n tenantScope: {\n tenantId: unifiedState.tenantScope?.tenantId,\n scope: unifiedState.tenantScope?.scope,\n noGlobalDerivedState: unifiedState.tenantScope?.noGlobalDerivedState\n },\n narrativeFingerprint: unifiedState.narrativeFingerprint,\n screenshotScopeWatermark: unifiedState.screenshotScopeWatermark,\n interpretationSafetyContract: unifiedState.interpretationSafetyContract,\n interpretationUxContract: {\n schema: unifiedState.interpretationUxContract?.schema,\n layerOrder: unifiedState.interpretationUxContract?.layerOrder,\n narrativeNeverVisuallyDominant: unifiedState.interpretationUxContract?.narrativeNeverVisuallyDominant\n },\n governance: {\n narrativeIsNotDecisionLayer: unifiedState.governance.narrativeIsNotDecisionLayer,\n displayOrder: unifiedState.governance.displayOrder,\n trustPosture: unifiedState.governance.trustPosture,\n interpretationUxContract: unifiedState.governance.interpretationUxContract?.schema\n },\n humanOps: {\n decisionTier: unifiedState.humanOps?.humanDecisionScaling?.tier,\n decisionOwner: unifiedState.humanOps?.humanDecisionScaling?.decisionOwner,\n opsRouteId: unifiedState.humanOps?.humanDecisionOpsRunbook?.routing?.routeId,\n slaAckMinutes: unifiedState.humanOps?.humanDecisionOpsRunbook?.sla?.ackMinutes,\n uiDisclaimerRequired: unifiedState.humanOps?.narrativeUiSafety?.displayRules?.requireDisclaimerBanner,\n misreadHighResidual: unifiedState.humanOps?.misreadSimulation?.highResidualCount,\n propagationHighResidual: unifiedState.humanOps?.socialPropagationSimulation?.highResidualCount,\n worstWatermarkSurvivability:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.worstWatermarkSurvivability,\n dominantDistortionSource:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.dominantDistortionSource\n },\n culturalRisk: {\n perceptualRiskClass: unifiedState.culturalRisk?.perceptualRiskClass,\n trustFork: unifiedState.culturalRisk?.trustDynamics?.fork,\n trustDecayScore: unifiedState.culturalRisk?.trustDynamics?.scores?.trustDecay,\n mythologyScore: unifiedState.culturalRisk?.trustDynamics?.scores?.mythology,\n confidenceDetachmentExposure:\n unifiedState.culturalRisk?.trustDynamics?.axes?.confidenceDetachmentExposure\n },\n appliedSystemsLayer: {\n schema: unifiedState.appliedSystemsLayer?.schema,\n researchOutputType: unifiedState.appliedSystemsLayer?.researchGovernance?.outputType,\n roboticsBlockActuation: unifiedState.appliedSystemsLayer?.roboticsGrounding?.blockActuation,\n spiralRumorCount:\n unifiedState.appliedSystemsLayer?.spiralMMOGameKernel?.gameLayer?.rumors?.length\n },\n actionSemanticGovernance: {\n schema: unifiedState.actionSemanticGovernance?.schema,\n highRiskActionCount: unifiedState.actionSemanticGovernance?.highRiskActionCount,\n loopClosed: unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.loopClosed,\n authorityCollapseMitigated:\n unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.risks?.authorityCollapse\n ?.mitigated\n },\n actionContextResolution: {\n schema: unifiedState.actionContextResolution?.schema,\n contextFingerprint: unifiedState.actionContextResolution?.contextFingerprint?.fingerprintHash,\n domainCollision: unifiedState.actionContextResolution?.domainCollision?.detected,\n deployAgentSpiralBlocked:\n unifiedState.actionContextResolution?.actionOverloadingRisk?.deployAgentExample\n ?.spiral_mmo === \"blocked\",\n roboticsActuationEligible:\n unifiedState.actionContextResolution?.roboticsActuationCorrectness?.actuationEligible\n },\n epistemicCoherence: {\n schema: unifiedState.epistemicCoherence?.schema,\n verdict: unifiedState.epistemicCoherence?.systemCoherence?.verdict,\n coherenceScore: unifiedState.epistemicCoherence?.systemCoherence?.score,\n contradictionCount: unifiedState.epistemicCoherence?.crossLayerContradictions?.count,\n operatorDecision: unifiedState.epistemicCoherence?.uxCompression?.operatorDecision,\n headlineTr: unifiedState.epistemicCoherence?.uxCompression?.headline?.tr,\n falseSafetyWarning: Boolean(\n unifiedState.epistemicCoherence?.uxCompression?.falseSafetyWarning\n )\n },\n coherenceAuthorityBoundary: {\n schema: unifiedState.coherenceAuthorityBoundary?.schema,\n bindingValid: unifiedState.coherenceAuthorityBoundary?.eclAuthorityBinding?.valid,\n canExecute: unifiedState.coherenceAuthorityBoundary?.contract?.can_execute,\n mandatoryBannerTr:\n unifiedState.coherenceAuthorityBoundary?.disclaimers?.mandatoryBanner?.tr\n },\n decisionLatencyGovernance: {\n schema: unifiedState.decisionLatencyGovernance?.schema,\n latencyTier: unifiedState.decisionLatencyGovernance?.latencyTier,\n inflationScore: unifiedState.decisionLatencyGovernance?.latencyInflation?.score,\n fastPathEligible: unifiedState.decisionLatencyGovernance?.fastPath?.eligible,\n primaryActionId: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.primaryActionId,\n ackSlaMinutes: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.ackSlaMinutes,\n certaintyCap: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.certaintyCap,\n uncertaintyItems:\n unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.uncertaintyEnvelope?.itemCount\n },\n decisionPacketUncertaintyBoundary: {\n schema: unifiedState.decisionPacketUncertaintyBoundary?.schema,\n invariantValid: unifiedState.decisionPacketUncertaintyBoundary?.invariantCheck?.valid,\n centralInvariant: unifiedState.decisionPacketUncertaintyBoundary?.centralInvariant\n },\n epistemicDecisionPacing: {\n schema: unifiedState.epistemicDecisionPacing?.schema,\n queueSaturationTier:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation?.tier,\n operatorProcessingLatency:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation\n ?.operatorProcessingLatency,\n postureWindowCount:\n unifiedState.epistemicDecisionPacing?.temporaryStaticPostureWindows\n ?.temporaryStaticPostureWindows?.windowCount,\n executionBoundariesValid:\n unifiedState.epistemicDecisionPacing?.executionBoundaries?.valid,\n grantsExecutionApproval: unifiedState.epistemicDecisionPacing?.grantsExecutionApproval\n },\n epistemicTemporalCoherence: {\n schema: unifiedState.epistemicTemporalCoherence?.schema,\n allInvariantsValid: unifiedState.epistemicTemporalCoherence?.allInvariantsValid,\n temporalAlignmentValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.temporalAlignment?.valid,\n crossWindowGuardValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard?.valid,\n delayedTruthValid: unifiedState.epistemicTemporalCoherence?.invariants?.delayedTruthRule?.valid,\n reconciliationRequired:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard\n ?.reconciliation?.required\n },\n realityDriftObserver: {\n schema: unifiedState.realityDriftObserver?.schema,\n driftActive: unifiedState.realityDriftObserver?.driftObserverSummary?.active,\n misapprehensionShapeCount:\n unifiedState.realityDriftObserver?.misapprehensionShapeCatalog?.shapeCount,\n criticalShapeCount:\n unifiedState.realityDriftObserver?.driftObserverSummary?.criticalShapeCount,\n roboticsMismatchActive:\n unifiedState.realityDriftObserver?.roboticsFeedbackMismatch?.active\n },\n driftCausality: {\n schema: unifiedState.driftCausality?.schema,\n shapesWithCausalExplanation: unifiedState.driftCausality?.rdolLinkage?.shapesWithCausalExplanation,\n activeDomainCount: unifiedState.driftCausality?.causalitySummary?.activeDomainCount,\n propagationCausalityActive: unifiedState.driftCausality?.causalDomains?.propagation?.active\n },\n productReadiness: unifiedState.productReadiness,\n stateLayers: {\n raw: unifiedState.stateLayers.raw,\n derived: {\n systemState: unifiedState.stateLayers.derived.systemState,\n narrative: unifiedState.stateLayers.derived.narrative,\n validation: unifiedState.stateLayers.derived.validation\n },\n policy: {\n binding: unifiedState.stateLayers.policy.binding,\n decisionOwner: unifiedState.stateLayers.policy.decisionOwner,\n prohibitedActions: unifiedState.stateLayers.policy.prohibitedActions\n }\n },\n systemState: unifiedState.systemState,\n interpretation: unifiedState.interpretation,\n validation: {\n trustPosture: unifiedState.validation.trustPosture,\n adjustedConfidence: unifiedState.validation.adjustedConfidence,\n agreement: unifiedState.validation.confidenceDecomposition.agreement,\n divergenceScore: unifiedState.validation.divergence.divergenceScore,\n validated: unifiedState.validation.divergence.validated,\n uncertaintyTags: unifiedState.validation.uncertainty.tags.map((t) => t.id),\n flags: unifiedState.validation.divergence.flags.map((f) => f.id)\n },\n compression: unifiedState.compression\n }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 6 * 1024 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (\n !checkHttpRateLimit(`rhizoh_voice_transcribe:${rlKey}`, RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN, 60_000)\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const voiceEnv = rhizohVoiceTranscribeEnvV3();\n if (!voiceEnv.any) {\n sendJson(res, 503, {\n ok: false,\n error: \"voice_asr_not_configured\",\n env: voiceEnv\n });\n return;\n }\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n const result = await runRhizohVoiceTranscribeV3(\n applyLanguagePropagationToVoiceBodyV1(payload, langProp)\n );\n sendJson(res, result.ok ? 200 : 422, {\n ...result,\n traceId: String(payload?.traceId || \"\"),\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n atMs: Date.now()\n });\n } catch (error) {\n sendJson(res, 500, {\n ok: false,\n error: \"voice_transcribe_failed\",\n detail: String(error?.message || error),\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(\"/rhizoh/llm/task/\")) {\n try {\n const rawTaskId = decodeURIComponent(pathname.slice(\"/rhizoh/llm/task/\".length).split(\"/\")[0] || \"\");\n const taskId = String(rawTaskId || \"\").toLowerCase() === \"last\" ? getLatestLlmWorkerTaskIdV0() : rawTaskId;\n const snap = taskId ? getLlmWorkerTaskSnapshotV0(taskId) : null;\n if (!snap) {\n sendJson(res, 404, {\n ok: false,\n error: \"llm_task_not_found\",\n taskId: rawTaskId,\n status: \"missing\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n if (snap.status === \"processing\") {\n sendJson(res, 202, {\n ok: true,\n taskId: snap.taskId,\n status: \"processing\",\n createdAt: snap.createdAt,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n // Terminal states (completed/failed) always HTTP 200 — poll must not look like server fault (500).\n sendJson(res, 200, {\n ...(snap.body || { ok: false, error: \"llm_task_empty\", taskId: snap.taskId }),\n taskId: snap.taskId,\n status: snap.status === \"completed\" ? \"completed\" : snap.status,\n terminal: true,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n } catch (error) {\n console.error(\"[llm-task-poll] handler failed:\", error);\n sendJson(res, 200, {\n ok: false,\n error: \"llm_task_poll_handler_failed\",\n detail: String(error?.message || error),\n status: \"failed\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === rhizohRuntime.routes.rhizohLlm) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 128 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n logRhizohHealth(\"gateway_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: String(payload?.traceId || \"\"),\n auth: auth.ok ? \"ok\" : \"anon\",\n ip\n });\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (!checkHttpRateLimit(`rhizoh_llm:${rlKey}`, RL_RHIZOH_LLM_PER_MIN, 60_000)) {\n const rlTaxonomy = classifyStressResponseV0({ code: \"rate_limit_exceeded\" });\n return sendJson(res, 429, {\n ok: false,\n error: \"rate_limit_exceeded\",\n reply: rlTaxonomy.userMessageTr || \"İstek sınırı aşıldı. Kısa süre sonra tekrar deneyin.\",\n stressClass: rlTaxonomy.stressClass,\n responseAction: rlTaxonomy.responseAction,\n stressMatrix: rlTaxonomy.matrix,\n stressInterpretable: rlTaxonomy.interpretable,\n stressConfidence: rlTaxonomy.stressConfidence,\n actionConfidence: rlTaxonomy.actionConfidence,\n actionSoftened: rlTaxonomy.actionSoftened,\n responseActionStrict: rlTaxonomy.responseActionStrict,\n actionInterpretable: rlTaxonomy.actionInterpretable,\n conflictResolution: rlTaxonomy.conflictResolution,\n directive: \"NONE\"\n });\n }\n\n const requireExplicit = process.env.CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE === \"1\";\n const keyMode = normalizeClientLlmKeySource(payload?.llmKeySource ?? payload?.keySource);\n if (keyMode === null) {\n return sendJson(res, 400, {\n ok: false,\n error: \"invalid_llm_key_source\",\n reply: \"llmKeySource geçersiz. Kullanın: env | user_connection | auto\",\n directive: \"NONE\"\n });\n }\n if (requireExplicit && keyMode === \"auto\") {\n return sendJson(res, 400, {\n ok: false,\n error: \"llm_key_source_required\",\n reply: \"Sunucu CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE=1: istekte llmKeySource (env veya user_connection) zorunlu.\",\n directive: \"NONE\"\n });\n }\n\n const conn = auth.ok ? await resolveConnection(auth.uid, String(payload?.connectionId || \"\")) : null;\n\n if (keyMode === \"user_connection\") {\n if (!auth.ok) {\n return sendJson(res, 401, {\n ok: false,\n error: \"llm_auth_required_for_user_connection\",\n reply: \"Kayıtlı LLM bağlantısı için giriş gerekli.\",\n directive: \"NONE\"\n });\n }\n if (!conn?.apiKey) {\n return sendJson(res, 400, {\n ok: false,\n error: \"user_llm_connection_required\",\n reply: \"Geçerli bir LLM bağlantısı yok; Studio’da bağlantı ekleyin veya llmKeySource=env kullanın.\",\n directive: \"NONE\"\n });\n }\n }\n\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n let safePayload = applyLanguagePropagationToLlmPayloadV1({ ...(payload || {}) }, langProp);\n const trimOut = trimRhizohLlmGatewayPayloadV1(safePayload);\n if (trimOut.meta.trimmed) {\n logRhizohHealth(\"llm_payload_trimmed\", {\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n bytesBefore: trimOut.meta.bytesBefore,\n bytesAfter: trimOut.meta.bytesAfter\n });\n }\n safePayload = trimOut.payload;\n delete safePayload.apiKey;\n delete safePayload.llmKeySource;\n delete safePayload.keySource;\n\n const llmMessage = String(\n safePayload?.message || safePayload?.text || payload?.message || payload?.text || \"\"\n ).trim();\n if (!llmMessage) {\n return sendJson(res, 400, {\n ok: false,\n error: \"message_required\",\n reply: \"Mesaj metni gerekli (message alanı boş).\",\n directive: \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n if (!safePayload.message) {\n safePayload = { ...safePayload, message: llmMessage };\n }\n\n let resolvedProvider = payload?.provider;\n let resolvedModel = payload?.model;\n let connApiKey = \"\";\n\n ({\n resolvedProvider,\n resolvedModel,\n connApiKey\n } = resolveServerLlmProviderV0(keyMode, conn, payload));\n\n const turnInput = {\n safePayload,\n auth,\n keyMode,\n conn,\n resolvedProvider,\n resolvedModel,\n connApiKey\n };\n const turnMeta = {\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n langEcho: buildRhizohLanguagePropagationEchoV1(langProp)\n };\n\n if (\n isLlmWorkerEnabledV0() &&\n isLlmWorkerAsyncEnabledV0() &&\n String(req.headers[\"x-rhizoh-llm-sync\"] || \"\").trim() !== \"1\"\n ) {\n const taskId = enqueueLlmWorkerTaskAsyncV0(turnInput, turnMeta);\n logRhizohHealth(\"llm_worker_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n taskId,\n traceId: String(payload?.traceId || taskId),\n keyMode,\n async: true\n });\n sendJson(res, 202, {\n ok: true,\n taskId,\n status: \"processing\",\n traceId: String(payload?.traceId || taskId),\n pollPath: `/rhizoh/llm/task/${taskId}`,\n gatewayPid: process.pid,\n ...turnMeta.langEcho\n });\n return;\n }\n\n const { result, traceId, turnLatencyMs, spinePhases, sampledTrace } = isLlmWorkerEnabledV0()\n ? await runLlmWorkerTaskV0(turnInput, turnMeta)\n : await rhizohGatewayTurn(turnInput);\n logRhizohHealth(\"llm_response\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId,\n turnLatencyMs: Number(turnLatencyMs || 0),\n keyMode\n });\n sendJson(res, 200, {\n ...result,\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n traceId,\n turnLatencyMs,\n ...(spinePhases ? { spinePhases } : {}),\n ...(sampledTrace !== undefined ? { sampledTrace } : {})\n });\n } catch (error) {\n const msg = String(error?.message || \"\");\n const code = error?.code || \"\";\n let status = 500;\n if (msg === \"payload_too_large\") status = 413;\n else if (msg === \"rate_limit_exceeded\") status = 429;\n else if (\n code === \"cost_hard_limit\" ||\n code === \"phased_rollout_capacity\" ||\n error?.containment === true ||\n String(code).startsWith(\"agent_\")\n ) {\n status = 429;\n } else if (code === \"user_soft_blocked\" || code === \"prompt_abuse_detected\") status = 403;\n else if (\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(code) ||\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(msg) ||\n msg.includes(\"missing_api_key_for_\")\n )\n status = 400;\n let rhizohFailureKind = \"provider_error\";\n if (msg === \"rate_limit_exceeded\" || status === 429) rhizohFailureKind = \"rate_limit\";\n else if (status === 403) rhizohFailureKind = \"policy_block\";\n else if (status === 400 || status === 413) rhizohFailureKind = \"client_config\";\n else if (msg.startsWith(\"provider_http_\")) {\n const st = Number(msg.replace(\"provider_http_\", \"\"));\n if (st === 408 || st === 504) rhizohFailureKind = \"timeout\";\n else rhizohFailureKind = \"provider_error\";\n } else if (/timeout|timed out|ETIMEDOUT/i.test(msg)) rhizohFailureKind = \"timeout\";\n\n const providerHttpMatch = msg.match(/^provider_http_(\\d{3})$/);\n const stressTaxonomy =\n error?.stressClass != null\n ? {\n stressClass: error.stressClass,\n responseAction: error.responseAction,\n stressMatrix: error.stressMatrix,\n stressInterpretable: error.stressInterpretable !== false,\n stressConfidence: error.stressConfidence,\n actionConfidence: error.actionConfidence,\n actionSoftened: error.actionSoftened,\n responseActionStrict: error.responseActionStrict,\n actionInterpretable: error.actionInterpretable,\n conflictResolution: error.conflictResolution,\n stressSecondary: error.stressSecondary,\n responseActions: error.responseActions\n }\n : (() => {\n const t = classifyStressResponseV0({\n code: code || msg,\n rhizohFailureKind,\n providerHttpStatus: providerHttpMatch\n ? Number(providerHttpMatch[1])\n : null\n });\n return {\n stressClass: t.stressClass,\n responseAction: t.responseAction,\n stressMatrix: t.matrix,\n stressInterpretable: t.interpretable,\n stressConfidence: t.stressConfidence,\n actionConfidence: t.actionConfidence,\n actionSoftened: t.actionSoftened,\n responseActionStrict: t.responseActionStrict,\n actionInterpretable: t.actionInterpretable,\n conflictResolution: t.conflictResolution,\n stressSecondary: t.stressSecondary,\n responseActions: t.responseActions\n };\n })();\n sendJson(res, status, {\n ok: false,\n rhizohFailureKind,\n ...(providerHttpMatch ? { providerHttpStatus: Number(providerHttpMatch[1]) } : {}),\n error: code || msg || \"rhizoh_llm_failed\",\n detail: msg || code || undefined,\n reply:\n error?.reply ||\n (status === 500 ? \"Rhizoh bağlantısı geçici olarak kesildi.\" : msg),\n directive: error?.directive || \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n ...stressTaxonomy\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicSeal) {\n try {\n if (!EPISTEMIC_SEAL_SECRET) {\n sendJson(res, 503, { ok: false, error: \"epistemic_seal_disabled\" });\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`epistemic_seal:${ip}`, RL_EPISTEMIC_SEAL_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const truth = body.truth_contract;\n if (!truth || typeof truth !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"truth_contract_required\" });\n return;\n }\n const sealBody = {\n truth_contract: truth,\n runtime_hash: String(body.runtime_hash || \"\"),\n model_route: {\n provider: body.model_route?.provider ?? null,\n model: body.model_route?.model ?? null\n },\n memory_digest: String(body.memory_digest || \"\"),\n world_snapshot_hash: String(body.world_snapshot_hash || \"\"),\n timestamp: Number(body.timestamp) || Date.now()\n };\n const canonical = canonicalEpistemicSealString(sealBody);\n const { hash, signature } = hashAndSignEpistemicSeal(canonical, EPISTEMIC_SEAL_SECRET);\n recordGenesisEpistemicSealIssued(hash);\n const attestation = {\n schema: EPISTEMIC_SEAL_SCHEMA,\n algorithm: \"SHA-256+HMAC-SHA256\",\n keyHint: process.env.CASTLE_EPISTEMIC_SEAL_SECRET ? \"CASTLE_EPISTEMIC_SEAL_SECRET\" : \"CASTLE_GATEWAY_TOKEN\",\n issuedAt: Date.now(),\n gateway: { service: \"castle-gateway\", listenPort: PORT }\n };\n sendJson(res, 200, {\n ok: true,\n hash,\n signature,\n attestation,\n canonicalBytes: Buffer.byteLength(canonical, \"utf8\")\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"seal_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicLogsBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`epistemic_logs:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 120) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const persisted = await persistEpistemicLedgerBatch(auth.uid, entries);\n recordGenesisEpistemicLedgerPersisted(persisted.persisted);\n // Internal cognition layer: forecast stays gateway/ledger-only (no client exposure).\n try {\n await persistEpistemicForecastBatch(auth.uid, persisted.normalized || []);\n } catch {\n /* forecast failures must not break primary ledger ingest */\n }\n sendJson(res, 200, {\n ok: true,\n persisted: persisted.persisted,\n mode: persisted.mode,\n latest: persisted.normalized?.[persisted.normalized.length - 1] || null\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"epistemic_log_ingest_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.authorityLedgerBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`authority_ledger:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 80) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const witnessed = persistAuthorityLedgerWitnessBatchV1(auth.uid, entries, EPISTEMIC_SEAL_SECRET);\n if (witnessed.witnessed > 0) {\n recordGenesisEpistemicLedgerPersisted(witnessed.witnessed);\n const clientSeal = witnessed.lastWitness?.clientSealHash;\n if (clientSeal) recordGenesisEpistemicSealIssued(clientSeal);\n }\n sendJson(res, 200, {\n ok: true,\n witnessed: witnessed.witnessed,\n quarantined: witnessed.quarantined,\n chainHeight: witnessed.chainHeight,\n chainHead: witnessed.chainHead,\n lastWitness: witnessed.lastWitness,\n results: witnessed.results\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"authority_ledger_witness_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.authorityLedgerReplay) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const replayUrl = new URL(String(req.url || \"\"), \"http://localhost\");\n const epochId = String(replayUrl.searchParams.get(\"epochId\") || \"\").trim() || undefined;\n const snapshot = getAuthorityLedgerWitnessSnapshotV1(auth.uid, epochId);\n const replay = replayAuthorityLedgerWitnessChainV1(auth.uid, epochId);\n sendJson(res, 200, {\n ok: true,\n snapshot,\n replay,\n schema: \"castle.rhizoh.authority_ledger_witness.v1.replay_surface\"\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"authority_ledger_replay_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.authorityEpochMerge) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`authority_epoch_merge:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const merged = assimilateAuthorityEpochMergeV1(auth.uid, body);\n if (!merged.ok) {\n sendJson(res, 400, merged);\n return;\n }\n sendJson(res, 200, {\n ok: true,\n mergeEvent: merged.mergeEvent,\n recentMerges: merged.recentMerges\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"authority_epoch_merge_failed\") });\n }\n return;\n }\n\n sendJson(res, 404, { ok: false, error: \"not_found\" });\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\nspiralWssForGenesis = wss;\nconst orchestrator = createOrchestrator();\nconst clientStats = new Map();\nlet broadcasterClientId = null;\nconst spiralState = {\n rooms: [{ id: \"room-default\", name: \"Spiral Default Arena\" }],\n activeByClient: new Map(),\n characters: new Map() // id -> {id,name,role,roomId,source}\n};\n\n/** Castle social rooms — keyed by `castleRoomKey` from {@link WS_MESSAGE.CASTLE_SOCIAL_PULSE}. */\nconst castleSocialRoomState = new Map();\nconst CASTLE_SOCIAL_MEMBER_TTL_MS = 90_000;\nconst CASTLE_SOCIAL_MAX_PULSE_BYTES = 8192;\nconst RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES = 5 * 1024 * 1024;\nconst RHIZOH_VOICE_LIVE_MAX_CHUNKS = 96;\n\n/**\n * Voice Engine v3 live media lane.\n * Client streams MediaRecorder chunks over the existing gateway WS; STOP reuses the\n * gateway ASR runner so orchestration remains the single authority downstream.\n */\nconst rhizohVoiceLiveSessions = new WeakMap();\n\nfunction appendRhizohVoiceLiveEventV0(session, type, payloadRefSeed = \"\") {\n if (!session) return null;\n session.eventSeq = (Number(session.eventSeq) || 0) + 1;\n const event = createRhizohImmutableEventV0({\n type,\n sessionId: session.sessionId,\n traceId: session.traceId,\n actorSeed: session.sessionId || session.traceId,\n eventSeq: session.eventSeq,\n payloadRef: createRhizohPayloadRefV0(payloadRefSeed || `${session.sessionId}:${type}:${session.eventSeq}`)\n });\n session.events = [...(session.events || []), event].slice(-64);\n return event;\n}\n\nfunction sendRhizohVoiceLiveEnvelopeV0(socket, type, payload) {\n try {\n if (socket?.readyState === 1) socket.send(JSON.stringify(createEnvelope(type, payload)));\n } catch {\n /* noop */\n }\n}\n\nfunction decodeRhizohVoiceLiveChunkV0(payload = {}) {\n const b64 = String(payload.audioBase64 || \"\").trim();\n if (!b64) return { ok: false, error: \"audio_base64_required\" };\n try {\n const buf = Buffer.from(b64, \"base64\");\n if (!buf.length) return { ok: false, error: \"audio_decode_empty\" };\n return { ok: true, buffer: buf };\n } catch {\n return { ok: false, error: \"audio_base64_invalid\" };\n }\n}\n\nfunction handleRhizohVoiceLiveStartV0(socket, payload = {}) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const ctx = resolveVoiceWorldContextV0(p);\n const sessionId = String(ctx.sessionId).slice(0, 96);\n const session = {\n sessionId,\n worldId: ctx.worldId,\n boundMatchSessionId: ctx.boundMatchSessionId,\n traceId: String(p.traceId || \"\").slice(0, 128),\n mimeType: String(p.mimeType || \"audio/webm\").slice(0, 80),\n languageCode: String(p.languageCode || \"tr-TR\").slice(0, 32),\n path: String(p.path || \"both\").slice(0, 24),\n chunks: [],\n chunkIndexes: [],\n totalBytes: 0,\n nextChunkIndex: 1,\n eventSeq: 0,\n events: [],\n startedAtMs: Date.now()\n };\n rhizohVoiceLiveSessions.set(socket, session);\n registerVoiceGatewayCitizenV0(socket, {\n sessionId,\n worldId: ctx.worldId,\n boundMatchSessionId: ctx.boundMatchSessionId,\n traceId: session.traceId\n });\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SESSION_STARTED\", `${sessionId}:live_session`);\n sendRhizohVoiceLiveEnvelopeV0(\n socket,\n WS_MESSAGE.RHIZOH_VOICE_LIVE_START,\n enrichVoiceLivePayloadV0(session, \"VOICE_SESSION_START\", {\n ok: true,\n sessionId,\n schema: \"castle.rhizoh.voice_live.gateway_lane.v0\"\n })\n );\n}\n\nfunction handleRhizohVoiceLiveChunkV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const sequence = validateRhizohVoiceLiveChunkPayloadV0(payload, {\n expectedChunkIndex: session.nextChunkIndex\n });\n if (!sequence.ok) {\n rhizohVoiceLiveSessions.delete(socket);\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:chunk_reject`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: sequence.error,\n expectedChunkIndex: sequence.expectedChunkIndex,\n chunkIndex: sequence.chunkIndex\n });\n return;\n }\n const decoded = decodeRhizohVoiceLiveChunkV0(payload);\n if (!decoded.ok) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: decoded.error\n });\n return;\n }\n if (\n session.chunks.length >= RHIZOH_VOICE_LIVE_MAX_CHUNKS ||\n session.totalBytes + decoded.buffer.length > RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES\n ) {\n rhizohVoiceLiveSessions.delete(socket);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_audio_too_large\"\n });\n return;\n }\n session.chunks.push(decoded.buffer);\n session.chunkIndexes.push(sequence.chunkIndex);\n session.totalBytes += decoded.buffer.length;\n session.nextChunkIndex += 1;\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_CHUNK_ACCEPTED\", `${session.sessionId}:chunk:${sequence.chunkIndex}`);\n}\n\nfunction handleRhizohVoiceLiveStopV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n rhizohVoiceLiveSessions.delete(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const lastChunkIndex = Number(payload?.lastChunkIndex);\n if (Number.isInteger(lastChunkIndex) && lastChunkIndex !== session.chunks.length) {\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:stop_mismatch`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_chunk_count_mismatch\",\n expectedChunkCount: lastChunkIndex,\n receivedChunkCount: session.chunks.length\n });\n return;\n }\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_STOP_RECEIVED\", `${session.sessionId}:stop:${session.chunks.length}`);\n const audio = Buffer.concat(session.chunks);\n if (!audio.length) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"audio_empty\"\n });\n return;\n }\n runRhizohVoiceTranscribeV3({\n path: String(payload?.path || session.path || \"both\"),\n audioBase64: audio.toString(\"base64\"),\n mimeType: String(payload?.mimeType || session.mimeType || \"audio/webm\"),\n languageCode: String(payload?.languageCode || session.languageCode || \"tr-TR\"),\n traceId: String(payload?.traceId || session.traceId || \"\"),\n sessionId: session.sessionId,\n transport: \"gateway_ws_live\"\n })\n .then((result) => {\n appendRhizohVoiceLiveEventV0(session, result?.ok ? \"VOICE_TRANSCRIBE_FINAL\" : \"VOICE_TRANSCRIBE_FAILED\", `${session.sessionId}:final`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_FINAL, {\n ...result,\n sessionId: session.sessionId,\n transportPath: \"gateway_ws_live\",\n bytes: audio.length,\n chunkCount: session.chunks.length,\n chunkSequence: Object.freeze({\n ok: true,\n firstChunkIndex: session.chunkIndexes[0] || null,\n lastChunkIndex: session.chunkIndexes[session.chunkIndexes.length - 1] || null,\n receivedChunkCount: session.chunks.length\n }),\n eventTrace: Object.freeze((session.events || []).slice(-16)),\n recordedMs: Math.max(0, Date.now() - session.startedAtMs)\n });\n })\n .catch((e) => {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_transcribe_failed\",\n detail: String(e?.message || e)\n });\n });\n}\n\nfunction removeCastleSocialClientFromAllRooms(clientId) {\n const cid = String(clientId || \"\");\n if (!cid) return;\n for (const room of castleSocialRoomState.values()) {\n room.members.delete(cid);\n }\n}\n\nfunction buildCastleSocialRoomPayload(roomKey) {\n const room = castleSocialRoomState.get(roomKey);\n if (!room) return null;\n const now = Date.now();\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const roster = [...room.members.values()].map((m) => ({\n userId: m.userId,\n lastMs: m.lastMs,\n ...m.sp\n }));\n const peerFeeds =\n room.walPeers && room.walPeers.size\n ? [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }))\n : [];\n return { castleRoomKey: roomKey, seq: room.seq, roster, peerFeeds, ts: now };\n}\n\nconst CASTLE_WAL_PEER_MAX_FEED_BYTES = 48 * 1024;\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {{ ok: boolean, code?: string, reason?: string }} gate\n */\nfunction rejectWalPeerIngressV0(socket, gate) {\n const code = gate.code || \"wal_peer_rejected\";\n recordWalPeerRejectV0(code);\n recordSubstrateOtelEventV0(\"wal_peer_reject\", { code, reason: String(gate.reason || \"\").slice(0, 120) });\n socket.send(\n JSON.stringify(\n createEnvelope(WS_MESSAGE.ERROR, {\n error: gate.reason || \"WAL peer ingress rejected.\",\n code\n })\n )\n );\n}\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {Record<string, unknown> | null | undefined} walPeerFeed\n * @returns {boolean}\n */\nfunction gateWalPeerIngressV0(socket, walPeerFeed) {\n const authGate = assertWalPeerSocketAuthorityV0(socket);\n if (!authGate.ok) {\n rejectWalPeerIngressV0(socket, authGate);\n return false;\n }\n const sigGate = validateWalPeerFeedSignatureV0(\n walPeerFeed && typeof walPeerFeed === \"object\" ? walPeerFeed : {}\n );\n if (!sigGate.ok) {\n rejectWalPeerIngressV0(socket, sigGate);\n return false;\n }\n return true;\n}\n\nfunction handleCastleWalPeerFeed(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_WAL_PEER_MAX_FEED_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_WAL_PEER_FEED too large.\" })));\n return;\n }\n const walPeerFeed = p.walPeerFeed && typeof p.walPeerFeed === \"object\" ? p.walPeerFeed : {};\n if (!gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const castleRoomKey = String(p.castleRoomKey || \"wal:convergence\").slice(0, 64);\n const castleId = String(p.castleId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId,\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: room.members.get(socket.clientId)?.sp ?? {}\n });\n const peerFeeds = [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }));\n const body = {\n castleRoomKey,\n seq: room.seq,\n peerFeeds,\n ts: now\n };\n const env = createEnvelope(WS_MESSAGE.CASTLE_WAL_PEER_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys(), ...room.walPeers.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction handleCastleSocialPulse(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_SOCIAL_MAX_PULSE_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_SOCIAL_PULSE too large.\" })));\n return;\n }\n const castleRoomKey = String(p.castleRoomKey || \"default\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n const sp = p.socialPulse && typeof p.socialPulse === \"object\" ? p.socialPulse : {};\n const walPeerFeed =\n p.walPeerFeed && typeof p.walPeerFeed === \"object\"\n ? p.walPeerFeed\n : sp.walPeerFeed && typeof sp.walPeerFeed === \"object\"\n ? sp.walPeerFeed\n : null;\n if (walPeerFeed && !gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const spNext = walPeerFeed ? { ...sp, walPeerFeed } : { ...sp };\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: spNext\n });\n if (walPeerFeed) {\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId: String(p.castleId || userId).slice(0, 64),\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n }\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const body = buildCastleSocialRoomPayload(castleRoomKey);\n if (!body) return;\n const env = createEnvelope(WS_MESSAGE.CASTLE_SOCIAL_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcast(msgObj) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcastState() {\n broadcast(\n createEnvelope(WS_MESSAGE.BROADCAST_STATE, {\n broadcasterClientId,\n viewers: [...wss.clients].filter((c) => c.readyState === 1).length\n })\n );\n}\n\nfunction currentSpiralStatePayload() {\n return {\n rooms: spiralState.rooms,\n characters: [...spiralState.characters.values()]\n };\n}\n\nfunction broadcastSpiralState() {\n broadcast(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload()));\n}\n\nfunction ensureRoom(roomId, fallbackName = null) {\n if (!roomId) return spiralState.rooms[0].id;\n const exists = spiralState.rooms.find((r) => r.id === roomId);\n if (exists) return exists.id;\n const created = { id: roomId, name: fallbackName || `Spiral-${roomId.slice(-4)}` };\n spiralState.rooms.push(created);\n return created.id;\n}\n\nfunction syncSpiralCharactersWithWorld(snapshot) {\n const agents = snapshot?.agents || [];\n for (const a of agents) {\n if (!spiralState.characters.has(a.id)) {\n spiralState.characters.set(a.id, {\n id: a.id,\n name: a.id,\n role: \"agent\",\n roomId: spiralState.rooms[0].id,\n source: \"world\"\n });\n }\n }\n}\n\nfunction broadcastExcept(msgObj, exceptSocket) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client !== exceptSocket && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction validateInputFrame(payload, socket) {\n const stats = clientStats.get(socket.clientId) || { lastFrame: 0, windowStart: Date.now(), msgCount: 0 };\n const now = Date.now();\n if (now - stats.windowStart > 5000) {\n stats.windowStart = now;\n stats.msgCount = 0;\n }\n stats.msgCount += 1;\n clientStats.set(socket.clientId, stats);\n if (stats.msgCount > 120) return \"Rate limit exceeded.\";\n\n if (!payload || !Number.isInteger(payload.frame)) return \"frame must be integer.\";\n if (payload.frame <= stats.lastFrame) return \"frame must be monotonic.\";\n const commands = Array.isArray(payload.commands) ? payload.commands : [];\n if (commands.length > 8) return \"Too many commands in one frame.\";\n for (const cmd of commands) {\n if (!cmd || typeof cmd.command !== \"string\") return \"Invalid command format.\";\n if (\n cmd.command !== \"SPAWN_AGENT\" &&\n cmd.command !== \"LIST_CASTLES\" &&\n cmd.command !== \"SPAWN_ENTITY\" &&\n cmd.command !== \"CREATE_CASTLE\"\n ) return \"Unsupported command.\";\n }\n stats.lastFrame = payload.frame;\n clientStats.set(socket.clientId, stats);\n return null;\n}\n\nfunction validateSignalPayload(payload) {\n if (!payload || typeof payload !== \"object\") return \"Invalid signaling payload.\";\n const signalType = payload.signalType;\n if (Object.values(CASTLE_NETWORK_SIGNAL_V0).includes(signalType)) {\n return null;\n }\n if (![\"OFFER\", \"ANSWER\", \"ICE\"].includes(signalType)) return \"Invalid signal type.\";\n if (typeof payload.to !== \"string\" || payload.to.length < 3) return \"Invalid signal destination.\";\n if (signalType !== \"ICE\" && JSON.stringify(payload.sdp || {}).length > 16 * 1024) return \"SDP too large.\";\n if (signalType === \"ICE\" && JSON.stringify(payload.candidate || {}).length > 6 * 1024) return \"ICE too large.\";\n return null;\n}\n\nfunction sanitizeRawMessage(raw) {\n if (!raw) return null;\n const text = raw.toString();\n if (Buffer.byteLength(text, \"utf8\") > MAX_MESSAGE_BYTES) return null;\n return text;\n}\n\nwss.on(\"connection\", async (socket, req) => {\n const origin = req?.headers?.origin || \"\";\n if (!httpCors.isWebSocketOriginAllowed(origin)) {\n socket.close(1008, \"Origin not allowed\");\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const path = req?.url || \"\";\n const query = new URL(`ws://localhost${path}`).searchParams;\n const token = query.get(\"token\");\n if (!token || token !== REQUIRED_GATEWAY_TOKEN) {\n socket.close(1008, \"Invalid gateway token\");\n return;\n }\n }\n if (REQUIRE_AUTH) {\n const result = await verifyClientToken(req);\n if (!result.ok) {\n const isLocal = origin.includes(\"localhost\") || origin.includes(\"127.0.0.1\") || !origin;\n if (!(ALLOW_DEV_ANON && isLocal)) {\n socket.close(1008, result.reason);\n return;\n }\n socket.auth = { ok: true, kind: \"dev-anon\", user: { uid: \"dev-anon\" } };\n } else {\n socket.auth = result;\n }\n }\n\n socket.clientId = `c-${Math.random().toString(36).slice(2, 8)}`;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.HELLO, { message: \"Castle Gateway online.\", clientId: socket.clientId })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.WORLD_SNAPSHOT, orchestrator.worldSnapshot())));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.BROADCAST_STATE, { broadcasterClientId, viewers: [...wss.clients].filter((c) => c.readyState === 1).length })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload())));\n broadcastExcept(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }), socket);\n\n socket.on(\"message\", (raw) => {\n const safeRaw = sanitizeRawMessage(raw);\n if (!safeRaw) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Message too large or invalid.\" })));\n return;\n }\n const parsed = safeJsonParse(safeRaw);\n if (!parsed?.type) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid message.\" })));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND_TEXT) {\n const canonical = parseTextToCommand(parsed.payload?.text);\n if (!canonical) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Unknown command text.\" })));\n return;\n }\n const result = orchestrator.applyCommand(canonical);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.MATCH_SESSION_JOIN) {\n const joined = handleMatchSessionJoinV0(socket, parsed, wss);\n const sessionId = String(parsed.sessionId || parsed.payload?.sessionId || \"\").trim();\n if (joined?.ok && sessionId) {\n sendMatchSessionSnapshotOnJoinV0(socket, sessionId);\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.MATCH_CASTLE_INVITE) {\n handleMatchCastleInviteV0(socket, parsed, wss);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.MATCH_MOVE) {\n handleMatchMoveAuthorityV0(socket, parsed, wss);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.MATCH_STATE_APPLIED) {\n handleMatchStateAppliedV0(socket, parsed, wss);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.VOICE_STATE_APPLIED) {\n handleVoiceStateAppliedV0(socket, parsed);\n return;\n }\n\n\n if (parsed.type === WS_MESSAGE.COMMAND) {\n const result = orchestrator.applyCommand(parsed.payload);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n }\n\n if (parsed.type === WS_MESSAGE.INPUT_FRAME) {\n const err = validateInputFrame(parsed.payload, socket);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const frame = parsed.payload.frame;\n const commands = Array.isArray(parsed.payload?.commands) ? parsed.payload.commands : [];\n orchestrator.queueInputFrame(socket.clientId, frame, commands);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, queued: true, frame })));\n }\n\n if (parsed.type === WS_MESSAGE.SIGNAL) {\n const payload = parsed.payload || {};\n if (Object.values(CASTLE_NETWORK_SIGNAL_V0).includes(payload.signalType)) {\n const netErr = validateCastleNetworkSignalPayloadV0(socket, payload);\n if (netErr) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: netErr })));\n return;\n }\n handleCastleNetworkSignalV0(socket, payload, wss);\n return;\n }\n const err = validateSignalPayload(payload);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const signalType = payload.signalType;\n const to = payload.to;\n for (const client of wss.clients) {\n if (client.clientId === to && client.readyState === 1) {\n client.send(JSON.stringify(createEnvelope(WS_MESSAGE.SIGNAL, { ...payload, from: socket.clientId })));\n break;\n }\n }\n }\n\n if (parsed.type === WS_MESSAGE.DRONE_TELEMETRY) {\n const payload = parsed.payload || {};\n const json = JSON.stringify(payload);\n if (Buffer.byteLength(json, \"utf8\") > 48 * 1024) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"DRONE_TELEMETRY payload too large.\" })));\n return;\n }\n broadcastExcept(createEnvelope(WS_MESSAGE.DRONE_TELEMETRY, { ...payload, relayFrom: socket.clientId }), socket);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_START) {\n handleRhizohVoiceLiveStartV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_CHUNK) {\n handleRhizohVoiceLiveChunkV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_STOP) {\n handleRhizohVoiceLiveStopV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_SOCIAL_PULSE) {\n handleCastleSocialPulse(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_WAL_PEER_FEED) {\n handleCastleWalPeerFeed(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.OPEN_DATA_QUERY) {\n queryOpenData(parsed.payload)\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: true, result })));\n })\n .catch((e) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: false, error: e.message })));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_QUERY) {\n runRhizohBrainV2({\n ...(parsed.payload || {}),\n uid: socket?.auth?.user?.uid || \"unknown\"\n })\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, result)));\n })\n .catch(async () => {\n const fallback = await runRhizohBrain(parsed.payload || {}).catch(() => ({\n ok: false,\n reply: \"Rhizoh temporary disturbance. Habitat remains stable.\"\n }));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, fallback)));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.BROADCAST_REGISTER) {\n const role = String(parsed.payload?.role || \"\");\n const serviceKind = String(parsed.payload?.kind || parsed.payload?.serviceKind || \"\").trim();\n const serviceId = String(parsed.payload?.serviceId || role || socket.clientId || \"\").trim();\n if (serviceKind && serviceId) {\n registerGatewayServiceNodeV0({\n kind: serviceKind,\n serviceId,\n gatewayClientId: socket.clientId,\n state: String(parsed.payload?.state || \"ONLINE\"),\n meta: parsed.payload?.meta\n });\n }\n if (role === \"GENESIS_BROADCAST_AGENT\") {\n broadcasterClientId = socket.clientId;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, registered: role })));\n broadcastState();\n } else if (serviceKind) {\n socket.send(\n JSON.stringify(\n createEnvelope(WS_MESSAGE.COMMAND_RESULT, {\n ok: true,\n registered: serviceKind,\n serviceId,\n interpretationOnly: true\n })\n )\n );\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.STUDIO_CUE) {\n // Only registered broadcaster can push global cues.\n if (!broadcasterClientId || socket.clientId !== broadcasterClientId) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Only broadcaster agent can emit STUDIO_CUE.\" })));\n return;\n }\n const cue = parsed.payload || {};\n if (typeof cue !== \"object\") {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid STUDIO_CUE payload.\" })));\n return;\n }\n broadcast(createEnvelope(WS_MESSAGE.STUDIO_CUE, cue));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_JOIN_ROOM) {\n const requested = String(parsed.payload?.roomId || \"\").trim();\n const requestedName = String(parsed.payload?.roomName || \"\").trim();\n const roomId = ensureRoom(requested || `room-${Math.random().toString(36).slice(2, 7)}`, requestedName || null);\n spiralState.activeByClient.set(socket.clientId, roomId);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, roomId })));\n broadcastSpiralState();\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_CREATE_CHARACTER) {\n const name = String(parsed.payload?.name || \"\").trim().slice(0, 32) || `AI-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;\n const role = String(parsed.payload?.role || \"agent\").trim().slice(0, 24) || \"agent\";\n const roomId = ensureRoom(\n String(parsed.payload?.roomId || spiralState.activeByClient.get(socket.clientId) || spiralState.rooms[0].id),\n null\n );\n\n // Character creation always maps to an authoritative world agent.\n const result = orchestrator.applyCommand({\n command: COMMAND.SPAWN_AGENT,\n params: { targetCastleId: \"castle-01\" }\n });\n if (!result?.ok || !result?.data?.id) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Character creation failed.\" })));\n return;\n }\n const id = result.data.id;\n spiralState.characters.set(id, { id, name, role, roomId, source: \"spawned\" });\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, characterId: id, roomId })));\n broadcastSpiralState();\n return;\n }\n });\n\n socket.on(\"close\", () => {\n if (socket.clientId === broadcasterClientId) broadcasterClientId = null;\n leaveMatchBroadcastRoomV0(socket);\n unregisterGatewayServiceNodesByClientV0(socket.clientId);\n clientStats.delete(socket.clientId);\n rhizohVoiceLiveSessions.delete(socket);\n spiralState.activeByClient.delete(socket.clientId);\n removeCastleSocialClientFromAllRooms(socket.clientId);\n removeCastleNetworkClientV0(socket.clientId, wss);\n broadcast(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }));\n broadcastState();\n });\n});\n\nsetInterval(() => {\n void (async () => {\n try {\n const { frame, snapshot, delta, packedDelta } = orchestrator.tick(1 / 20);\n syncSpiralCharactersWithWorld(snapshot);\n broadcast(createEnvelope(WS_MESSAGE.NETWORK_TICK, { frame }));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_TICK, snapshot));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA, delta));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA_PACKED, packedDelta));\n broadcastSpiralState();\n await runAcademyEventTick();\n const executed = await tickAutomations(Date.now());\n for (const a of executed) {\n await addTranscript(a.uid, {\n source: \"planner\",\n eventType: \"automation-executed\",\n text: `Automation executed: ${a.title} (${a.action})`,\n roomId: \"studio-main\",\n meta: { automationId: a.id, action: a.action, deviceId: a.deviceId || \"\" }\n });\n }\n } catch (e) {\n console.error(\"[GATEWAY] world tick / automations loop error:\", String(e?.message || e));\n }\n })();\n}, 50);\n\n(async () => {\n await rhizohRuntime.telemetry.initOpenTelemetry();\n if (String(process.env.CASTLE_GENESIS_DISK_PERSIST ?? \"\").trim() === \"\") {\n process.env.CASTLE_GENESIS_DISK_PERSIST = \"1\";\n }\n const genesisDiskBoot = await hydrateGenesisContinuityPersistenceBootV0();\n if (!genesisDiskBoot.ok) {\n console.warn(\"[GATEWAY] genesis disk hydrate failed; continuing with in-memory genesis continuity\");\n }\n httpServer.listen(PORT, () => {\n startGenesisCanonicalClock();\n installGenesisCheckpointSurfaceGetter(buildGenesisRuntimeSurfacePayloadLive);\n setGenesisContinuityAfterPublishHook((seq) => {\n noteGenesisCheckpointSeqCommitted(seq);\n noteGenesisContinuitySeqAcceptedV0(seq);\n });\n startGenesisContinuityInfraSampler(buildGenesisRuntimeSurfacePayloadLive, 2000);\n logProductionObservatorySurfaceGuardsV0();\n logGatewaySubstrateAuthorityGuardsV0();\n if (isLlmWorkerEnabledV0()) {\n try {\n const workerBoot = initLlmWorkerAtBootV0();\n console.log(\"[GATEWAY] llm worker boot:\", workerBoot);\n } catch (workerErr) {\n console.error(\"[GATEWAY] llm worker boot failed:\", workerErr);\n }\n }\n console.log(`[GATEWAY] ws/http://localhost:${PORT}`);\n console.log(\n `[GATEWAY] genesis continuity: GET ${rhizohRuntime.routes.genesisRuntime} | POST ${rhizohRuntime.routes.genesisIngress} | SSE ${rhizohRuntime.routes.genesisStream} | replay ${rhizohRuntime.routes.genesisReplay} | diff ${rhizohRuntime.routes.genesisReplayDiff} | equiv ${rhizohRuntime.routes.genesisReplayEquivalence} | analytics ${rhizohRuntime.routes.genesisReplayAnalytics} | evolution ${rhizohRuntime.routes.genesisReplayEvolution}`\n );\n if (String(process.env.CASTLE_GENESIS_OBSERVATORY_LIVE_V01 ?? \"\").trim() === \"1\") {\n console.log(\"GENESIS OBSERVATORY LIVE v0.1 — READ ONLY PHASE STARTED\");\n }\n });\n})().catch((e) => {\n console.error(\"[GATEWAY] fatal boot (before or during listen):\", e);\n process.exit(1);\n});\n"}, "0d51787a0b0919184364614c2527c1328b078ba7": {"t": 1785754116.8352182, "n": 179165, "h": "0ca3c5068384efc5", "c": "import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport dotenv from \"dotenv\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconsole.log(\"🔥 [GENESIS_BOOT] entry file:\", __filename);\nconsole.log(\"🔥 [GENESIS_BOOT] node env:\", process.env.NODE_ENV);\nconsole.log(\"🔥 [GENESIS_BOOT] pid:\", process.pid);\nconsole.log(\"🧬 [GENESIS_BOOT] cwd:\", process.cwd());\nconsole.log(\"🧬 [GENESIS_BOOT] import.meta.url:\", import.meta.url);\ndotenv.config({ path: path.join(__dirname, \"..\", \".env\") });\ndotenv.config({ path: path.join(__dirname, \"..\", \".env.local\"), override: true });\n\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport { createServer } from \"node:http\";\nimport { createOrchestrator } from \"@castle/orchestrator\";\nimport { parseTextToCommand } from \"@castle/command-dsl\";\nimport {\n WS_MESSAGE,\n COMMAND,\n createEnvelope,\n createRhizohImmutableEventV0,\n createRhizohPayloadRefV0,\n safeJsonParse,\n validateRhizohVoiceLiveChunkPayloadV0\n} from \"@castle/protocol\";\nimport { queryOpenData } from \"./openData.js\";\nimport { verifyClientToken } from \"./auth.js\";\nimport { handleMatchMoveAuthorityV0, leaveMatchBroadcastRoomV0, sendMatchSessionSnapshotOnJoinV0 } from \"./rhizoh/matchMoveAuthorityV0.js\";\nimport { handleMatchSessionJoinV0, handleMatchCastleInviteV0 } from \"./rhizoh/matchBroadcastRoomV0.js\";\nimport { handleMatchStateAppliedV0 } from \"./rhizoh/matchAckAggregatorV0.js\";\nimport {\n listUnifiedGatewayPresenceV0,\n registerGatewayServiceNodeV0,\n unregisterGatewayServiceNodesByClientV0\n} from \"./rhizoh/gatewayPresenceRegistryV0.js\";\nimport { queryRhizohLlm } from \"./rhizohLlmGateway.js\";\nimport { rhizohGatewayTurn } from \"./rhizohGatewayTurn.js\";\nimport {\n enqueueLlmWorkerTaskAsyncV0,\n getLatestLlmWorkerTaskIdV0,\n getLlmWorkerDebugSnapshotV0,\n getLlmWorkerTaskSnapshotV0,\n initLlmWorkerAtBootV0,\n isLlmWorkerAsyncEnabledV0,\n isLlmWorkerEnabledV0,\n runLlmWorkerTaskV0\n} from \"./llmWorkerRuntime.js\";\nimport {\n RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n runRhizohVoiceTranscribeV3,\n rhizohVoiceTranscribeEnvV3\n} from \"./rhizohVoiceTranscribeV3.js\";\nimport { createHttpCorsPolicy, normalizeHttpOrigin, parseAllowedOriginsFromEnv } from \"./httpCorsPolicyV1.js\";\nimport {\n applyLanguagePropagationToLlmPayloadV1,\n applyLanguagePropagationToVoiceBodyV1,\n buildRhizohLanguagePropagationEchoV1,\n parseRhizohLanguagePropagationV1\n} from \"./rhizohLanguagePropagationV1.js\";\nimport { trimRhizohLlmGatewayPayloadV1 } from \"./rhizohLlmPayloadTrimV1.js\";\nimport {\n meshAppendDelta,\n meshContinuityAggregate,\n meshJoin,\n meshLeave,\n meshReplayFromNodeId,\n meshReplayFromSeq,\n meshSnapshot,\n meshSubscribeSse\n} from \"./presenceRoomMesh.js\";\nimport { runRhizohBrainV2 } from \"./rhizohBrainV2.js\";\nimport {\n listConnections,\n createConnection,\n updateConnection,\n deleteConnection,\n setDefaultConnection,\n resolveConnection\n} from \"./llmConnectionsStore.js\";\nimport { getCloudSyncSnapshotV0, mergeCloudSyncSnapshotV0 } from \"./castleCloudSyncStore.js\";\nimport {\n CASTLE_NETWORK_SIGNAL_V0,\n handleCastleNetworkSignalV0,\n listCastleNetworkPresenceV0,\n removeCastleNetworkClientV0,\n validateCastleNetworkSignalPayloadV0\n} from \"./castleNetworkRelayV0.js\";\nimport { checkHttpRateLimit, getHttpClientIp } from \"./castleHttpRateLimit.js\";\nimport {\n submitAbuseReportV0,\n listModerationQueueV0,\n softBlockUserV0,\n removeSoftBlockV0,\n listSoftBlockedUsersV0\n} from \"./ops/moderationMvpV0.js\";\nimport { listRecentAgentSnapshotsV0 } from \"./ops/agentObservabilityV0.js\";\nimport { readAgentContainmentConfigV0 } from \"./ops/agentContainmentV0.js\";\nimport { readCostContainmentConfigV0, getCostContainmentStatsV0 } from \"./ops/costContainmentV0.js\";\nimport { getGlobalCostLedgerSnapshotV0, getGclBackendStatusV0 } from \"./ops/globalCostLedgerV0.js\";\nimport { evaluateEconomicStrategyL3V0 } from \"./ops/economicStrategyEngineL3V0.js\";\nimport { getPhasedRolloutStatsV0, readPhasedRolloutTierV0 } from \"./ops/phasedRolloutV0.js\";\nimport { buildUnifiedStateNarrativeV0 } from \"./ops/unifiedStateNarrativeV0.js\";\nimport { resolveHardeningTenantIdV0 } from \"./ops/narrativeTenantIsolationV0.js\";\nimport {\n classifyStressResponseV0,\n verifyStressTaxonomyCoverageV0,\n verifyStressConflictResolutionV0\n} from \"./ops/stressResponseTaxonomyV0.js\";\nimport { verifyResolutionStabilityEnvelopeV0 } from \"./ops/resolutionStabilityEnvelopeV0.js\";\nimport { logLlmAccess } from \"./castleLlmAudit.js\";\nimport { appendMemory, listMemories, getMemoryContext, getPersonaGoalMemory, setPersonaGoalMemory, autoCompactMemories } from \"./memoryStore.js\";\nimport { getFirebasePersistence } from \"./firebasePersistence.js\";\nimport { registerAgentIdentity, listAgentIdentities, getAgentIdentity, updateAgentIdentity } from \"./agentIdentityStore.js\";\nimport { queueAcademyEvent, listAcademyEvents, resolveAcademyEvent, runAcademyEventTick } from \"./academyEventEngine.js\";\nimport { enqueuePdfJob, getPdfJob } from \"./pdfPipeline.js\";\nimport {\n createPublishSession,\n listPublishSessions,\n updatePublishSession,\n addTranscript,\n listTranscripts,\n searchMetadata,\n registerDevice,\n listDevices,\n updateDevice,\n appendTelemetry,\n listTelemetry,\n createAutomation,\n listAutomations,\n tickAutomations,\n enqueueCommand,\n listCommands,\n updateCommandStatus,\n registerSocialChannel,\n listSocialChannels\n} from \"./studioOpsStore.js\";\nimport { metrics as infraMetrics } from \"./infra/metrics.js\";\nimport { renderRhizohPrometheusMetrics, rhizohEnterpriseMetrics } from \"./infra/rhizohEnterpriseMetrics.js\";\nimport { scoreHealth } from \"./infra/healthScore.js\";\nimport { getTraceById, listTracesBySession } from \"./infra/traceRegistry.js\";\nimport { canonicalEpistemicSealString, hashAndSignEpistemicSeal, EPISTEMIC_SEAL_SCHEMA } from \"./epistemicSeal.js\";\nimport { persistEpistemicLedgerBatch } from \"./epistemicLedgerStore.js\";\nimport { persistEpistemicForecastBatch } from \"./epistemicForecastStore.js\";\nimport {\n persistAuthorityLedgerWitnessBatchV1,\n getAuthorityLedgerWitnessSnapshotV1,\n replayAuthorityLedgerWitnessChainV1\n} from \"./authorityLedgerWitnessStoreV1.js\";\nimport { assimilateAuthorityEpochMergeV1 } from \"./authorityEpochMergeStoreV1.js\";\nimport { buildRhizohExternalGroundTruthPayload } from \"./rhizohExternalGroundTruthGateway.js\";\nimport { ingestRhizohExternalLossBatchHttp } from \"./rhizohExternalLossIngestGateway.js\";\nimport {\n ingestRhizohProductOutcomeHttp,\n listRhizohProductOutcomeAggregates,\n verifyRhizohOutcomeHmac,\n verifyRhizohOutcomeSourceToken\n} from \"./rhizohProductOutcomeIngestGateway.js\";\nimport { initRhizoh } from \"./rhizohProductionBootstrap.js\";\nimport { handleAcademicObservatoryExportGetV0 } from \"./rhizoh/academicObservatoryHttpV0.js\";\nimport { handleCouncilAnomalyReasoningPostV0 } from \"./council/rhizohEpistemicCouncilHttpV0.js\";\nimport {\n handleLiveSportsBundleGetV0,\n handleLiveNewsHeadlinesGetV0,\n handleLiveWorldFeedGetV0\n} from \"./rhizoh/liveWorldFeedHttpV0.js\";\nimport {\n buildGenesisRuntimeSurfacePayload,\n recordGenesisEpistemicLedgerPersisted,\n recordGenesisEpistemicSealIssued,\n startGenesisCanonicalClock\n} from \"./genesisRuntimeSurfaceV0.js\";\nimport { registerGenesisSseClient, getGenesisContinuitySeq, setGenesisContinuityAfterPublishHook } from \"./genesisContinuityStreamHubV0.js\";\nimport { computeGenesisReplayFingerprintV0 } from \"./genesisReplayFingerprintV0.js\";\nimport { buildGenesisGatewayCapabilitiesV0 } from \"./genesisGatewayCapabilitiesV0.js\";\nimport {\n installGenesisCheckpointSurfaceGetter,\n noteGenesisCheckpointSeqCommitted,\n getLatestGenesisSignedCheckpoint\n} from \"./genesisContinuityCheckpointV0.js\";\nimport { hydrateGenesisContinuityPersistenceBootV0 } from \"./genesisContinuityHydrateBootV0.js\";\nimport { startGenesisContinuityInfraSampler } from \"./genesisContinuityInfraSamplerV0.js\";\nimport {\n genesisCheckpointQueryBySeqV0,\n genesisCheckpointQueryRangeV0,\n genesisCheckpointQueryLineageV0\n} from \"./genesisCheckpointQueryV0.js\";\nimport {\n genesisContinuityEventArchiveEnabled,\n queryGenesisContinuityEventArchiveV0,\n GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION\n} from \"./genesisContinuityEventArchiveV0.js\";\nimport { resolveGenesisReplayRouterV1, GENESIS_REPLAY_ROUTER_SCHEMA } from \"./genesisReplayRouterV1.js\";\nimport {\n computeGenesisReplayTemporalDiffV1,\n GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA\n} from \"./genesisReplayTemporalDiffV1.js\";\nimport { compareGenesisReplayEquivalenceV1 } from \"./genesisReplayEquivalenceV1.js\";\nimport { computeGenesisReplayAnalyticsV1 } from \"./genesisReplayAnalyticsV1.js\";\nimport { computeGenesisReplayEvolutionV1 } from \"./genesisReplayEvolutionV1.js\";\nimport { handleStripeCheckoutCreate, handleStripeMembershipWebhook } from \"./stripeWebhookMembership.js\";\nimport { buildRealityHealthPayload } from \"./edgeRealityConsistencyV1.js\";\nimport {\n assertWalPeerSocketAuthorityV0,\n buildGatewaySubstrateAuthoritySnapshotV0,\n logGatewaySubstrateAuthorityGuardsV0,\n validateWalPeerFeedSignatureV0\n} from \"./gatewaySubstrateAuthorityV0.js\";\nimport {\n buildSubstrateOperationalSnapshotV0,\n recordWalPeerRejectV0,\n renderSubstratePrometheusMetricsV0\n} from \"./infra/substrateOperationalMetrics.js\";\nimport { ingestClientSubstrateHealthV0 } from \"./rhizohSubstrateHealthIngestV0.js\";\nimport { recordSubstrateOtelEventV0 } from \"./infra/opentelemetrySubstrateV0.js\";\nimport {\n GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n getGenesisWorldObservationIngressStatusV0,\n ingestGenesisWorldObservationV0\n} from \"./genesisContinuityClientIngressV0.js\";\nimport {\n getGenesisContinuitySeqAuditV0,\n noteGenesisContinuitySeqAcceptedV0\n} from \"./genesisContinuitySeqGapV0.js\";\n\n/** Render / Fly / Railway: `PORT` — yoksa `CASTLE_GATEWAY_PORT` — yoksa 8090. */\nconst PORT =\n Number(process.env.PORT) ||\n Number(process.env.CASTLE_GATEWAY_PORT) ||\n 8090;\n/** Spiral / world WebSocket server — set after `wss` init; used by genesis runtime surface. */\nlet spiralWssForGenesis = null;\nconst MAX_MESSAGE_BYTES = Number(process.env.CASTLE_MAX_MESSAGE_BYTES || 32 * 1024);\nconst REQUIRED_GATEWAY_TOKEN = process.env.CASTLE_GATEWAY_TOKEN || \"\";\nconst ALLOWED_ORIGINS = parseAllowedOriginsFromEnv().fromList;\nconst httpCors = createHttpCorsPolicy();\n\n/** @returns {string|null} `Access-Control-Allow-Origin`; null = browser CORS fail. */\nfunction accessControlAllowOriginValue(req) {\n return httpCors.accessControlAllowOriginValue(req);\n}\n\nfunction applyHttpCorsHeaders(req, res) {\n return httpCors.applyHttpCorsHeaders(req, res);\n}\nconst REQUIRE_AUTH = process.env.CASTLE_REQUIRE_AUTH === \"true\";\nconst ALLOW_DEV_ANON = process.env.CASTLE_ALLOW_DEV_ANON !== \"false\";\nconst ALLOW_DEV_HTTP_UID = process.env.CASTLE_ALLOW_DEV_HTTP_UID !== \"false\";\n/** Presence mesh: allow `X-Castle-Guest-Id` when Firebase auth absent (default on). Set `false` to require auth only. */\nconst ALLOW_MESH_GUEST = process.env.CASTLE_ALLOW_MESH_GUEST !== \"false\";\nconst SOCIAL_RETRY_MAX = Math.max(0, Math.min(5, Number(process.env.CASTLE_SOCIAL_RETRY_MAX || 2)));\nconst SOCIAL_RETRY_BASE_MS = Math.max(100, Number(process.env.CASTLE_SOCIAL_RETRY_BASE_MS || 700));\nconst TELEGRAM_BOT_TOKEN = process.env.CASTLE_TELEGRAM_BOT_TOKEN || \"\";\nconst WHATSAPP_TOKEN = process.env.CASTLE_WHATSAPP_TOKEN || \"\";\nconst WHATSAPP_PHONE_NUMBER_ID = process.env.CASTLE_WHATSAPP_PHONE_NUMBER_ID || \"\";\nconst RL_RHIZOH_LLM_PER_MIN = Math.max(5, Number(process.env.CASTLE_RL_RHIZOH_LLM_PER_MIN || 40));\nconst RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN || 30)\n);\nconst RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN || 120));\nconst RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN = Math.max(\n 10,\n Number(process.env.CASTLE_RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN || 60)\n);\nconst RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN = Math.max(8, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN || 48));\nconst RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN = Math.max(4, Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN || 24));\nconst RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN || 18)\n);\nconst RL_EPISTEMIC_SEAL_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_EPISTEMIC_SEAL_PER_MIN || 120));\nconst RL_EPISTEMIC_LOG_PER_MIN = Math.max(20, Number(process.env.CASTLE_RL_EPISTEMIC_LOG_PER_MIN || 240));\nconst EPISTEMIC_SEAL_SECRET = String(\n process.env.CASTLE_EPISTEMIC_SEAL_SECRET || process.env.CASTLE_GATEWAY_TOKEN || \"\"\n).trim();\nconst RL_LLM_CONN_TEST_PER_MIN = Math.max(2, Number(process.env.CASTLE_RL_LLM_CONNECTION_TEST_PER_MIN || 8));\n\n/** One-shot production warnings: CORS surface, auth toggle, dev bypass flags (observatory deploy hygiene). */\nfunction logProductionObservatorySurfaceGuardsV0() {\n if (String(process.env.NODE_ENV || \"\").toLowerCase() !== \"production\") return;\n const tag = \"[GATEWAY_PROD_GUARD]\";\n const primaryCors = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n const corsListEmpty = ALLOWED_ORIGINS.length === 0;\n if (corsListEmpty && !primaryCors) {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty and CASTLE_HTTP_CORS_ORIGIN empty — production still merges RHIZOH_BROWSER_ORIGINS_V1 defaults; set env for preview/staging hosts`\n );\n } else if (corsListEmpty && primaryCors && primaryCors !== \"*\") {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty — only CASTLE_HTTP_CORS_ORIGIN set; add web.app + firebaseapp.com (and previews) to CASTLE_ALLOWED_ORIGINS if clients use alternate hostnames`\n );\n }\n if (primaryCors && /localhost|127\\.0\\.0\\.1/i.test(primaryCors)) {\n console.warn(`${tag} CORS: CASTLE_HTTP_CORS_ORIGIN is localhost while NODE_ENV=production`);\n }\n if (REQUIRE_AUTH) {\n console.warn(\n `${tag} AUTH: CASTLE_REQUIRE_AUTH=true — clients must send a valid gateway credential (otherwise silent 401 on replay/SSE); EventSource cannot set custom headers unless proxied`\n );\n if (REQUIRED_GATEWAY_TOKEN.length < 16) {\n console.warn(`${tag} AUTH: CASTLE_GATEWAY_TOKEN missing or short while require-auth enabled`);\n }\n }\n if (ALLOW_DEV_ANON) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_ANON is not \"false\" — dev anonymous path may be live in production (set CASTLE_ALLOW_DEV_ANON=false)`\n );\n }\n if (ALLOW_DEV_HTTP_UID) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_HTTP_UID is not \"false\" — X-Castle-Dev-Uid bypass risk (set CASTLE_ALLOW_DEV_HTTP_UID=false)`\n );\n }\n}\n\nconst rhizohRuntime = initRhizoh();\n\n/** İstemci: llmKeySource veya keySource — env | user_connection | auto */\nfunction normalizeClientLlmKeySource(raw) {\n const s = String(raw ?? \"\").trim().toLowerCase();\n if (s === \"env\" || s === \"server\") return \"env\";\n if (s === \"user_connection\" || s === \"connection\" || s === \"user\") return \"user_connection\";\n if (s === \"auto\" || s === \"\") return \"auto\";\n return null;\n}\n\n/**\n * Server-side provider wins for env/auto unless user_connection — client bundle may still send provider=openai.\n * @param {\"env\"|\"user_connection\"|\"auto\"} keyMode\n * @param {{ provider?: string, model?: string, apiKey?: string } | null} conn\n * @param {{ provider?: string, model?: string }} payload\n */\nfunction resolveServerLlmProviderV0(keyMode, conn, payload) {\n const envProvider = String(process.env.CASTLE_LLM_PROVIDER || \"\").trim() || undefined;\n const envModel = String(process.env.CASTLE_LLM_MODEL || \"\").trim() || undefined;\n\n if (keyMode === \"user_connection\") {\n return {\n resolvedProvider: conn?.provider || payload?.provider,\n resolvedModel: conn?.model || payload?.model,\n connApiKey: conn?.apiKey || \"\"\n };\n }\n\n if (keyMode === \"env\") {\n return {\n resolvedProvider: envProvider ?? payload?.provider,\n resolvedModel: envModel ?? payload?.model,\n connApiKey: \"\"\n };\n }\n\n if (conn?.provider && conn?.apiKey) {\n return {\n resolvedProvider: conn.provider,\n resolvedModel: conn.model,\n connApiKey: conn.apiKey\n };\n }\n\n return {\n resolvedProvider: envProvider ?? payload?.provider,\n resolvedModel: envModel ?? payload?.model,\n connApiKey: \"\"\n };\n}\n\nfunction jsonReplacerSafeV0(_key, value) {\n if (typeof value === \"bigint\") return Number(value);\n return value;\n}\n\nfunction sendJson(res, statusCode, payload) {\n res.writeHead(statusCode, { \"Content-Type\": \"application/json; charset=utf-8\" });\n try {\n res.end(JSON.stringify(payload, jsonReplacerSafeV0));\n } catch (error) {\n res.end(\n JSON.stringify({\n ok: false,\n error: \"response_serialize_failed\",\n detail: String(error?.message || error)\n })\n );\n }\n}\n\nfunction logRhizohHealth(stage, detail = {}) {\n try {\n console.info(`[CASTLE_${String(stage || \"unknown\")}]`, detail && typeof detail === \"object\" ? detail : {});\n } catch {\n /* noop */\n }\n}\n\nfunction renderInfraMetricsProm() {\n const lines = [\n `castle_gateway_events_processed_total ${infraMetrics.eventsProcessed}`,\n `castle_gateway_duplicate_reject_total ${infraMetrics.duplicateRejects}`,\n `castle_gateway_enqueue_latency_ms ${infraMetrics.enqueueLatencyMs}`,\n `castle_gateway_queue_depth ${infraMetrics.queueDepth}`,\n `castle_gateway_queue_lag_seconds ${(Number(infraMetrics.queueLag || 0) / 1000).toFixed(3)}`,\n `castle_gateway_errors_total ${infraMetrics.errors}`\n ];\n const buckets = infraMetrics.enqueueLatencyBuckets || {};\n for (const k of Object.keys(buckets)) {\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"${k}\"} ${buckets[k]}`);\n }\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"+Inf\"} ${infraMetrics.eventsProcessed}`);\n if (process.env.CASTLE_SUBSTRATE_METRICS !== \"0\") {\n lines.push(renderSubstratePrometheusMetricsV0().trimEnd());\n }\n return lines.join(\"\\n\");\n}\n\nasync function fetchJsonWithTimeout(url, timeoutMs = 1200) {\n const ctl = new AbortController();\n const tid = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const res = await fetch(url, { signal: ctl.signal });\n if (!res.ok) return null;\n return await res.json();\n } catch {\n return null;\n } finally {\n clearTimeout(tid);\n }\n}\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction getHttpPathname(req) {\n let p = String(req.url || \"\").split(\"?\")[0];\n /* Reverse proxies / clients sometimes append `/` — avoid silent 404 on e.g. `/rhizoh/genesis/stream/`. */\n while (p.length > 1 && p.endsWith(\"/\")) {\n p = p.slice(0, -1);\n }\n return p;\n}\n\nfunction rhizohLlmEnvConfigured() {\n const p = String(process.env.CASTLE_LLM_PROVIDER || \"openai\").toLowerCase();\n const key =\n p === \"anthropic\"\n ? process.env.ANTHROPIC_API_KEY\n : p === \"gemini\"\n ? process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY\n : p === \"xai\"\n ? process.env.XAI_API_KEY\n : p === \"deepseek\"\n ? process.env.DEEPSEEK_API_KEY\n : p === \"mistral\"\n ? process.env.MISTRAL_API_KEY\n : p === \"openrouter\"\n ? process.env.OPENROUTER_API_KEY\n : process.env.OPENAI_API_KEY;\n return !!String(key || \"\").trim();\n}\n\n/**\n * @param {import(\"firebase-admin/firestore\").Firestore | null} db\n * @param {number} timeoutMs\n */\nasync function probeFirestoreReachable(db, timeoutMs = 2000) {\n if (!db) return { ok: false, reason: \"no_db\" };\n const timeout = sleep(timeoutMs).then(() => ({ ok: false, reason: \"timeout\" }));\n const work = db\n .collection(\"_castle_gateway_health\")\n .doc(\"ping\")\n .get()\n .then(() => ({ ok: true, reason: \"reachable\" }))\n .catch((e) => ({ ok: false, reason: String(e?.message || e || \"firestore_error\") }));\n return Promise.race([work, timeout]);\n}\n\nfunction stripHtml(text = \"\") {\n return String(text)\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \" \")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \" \")\n .replace(/<[^>]+>/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction haversineMeters(aLat, aLon, bLat, bLon) {\n const R = 6371000;\n const dLat = ((bLat - aLat) * Math.PI) / 180;\n const dLon = ((bLon - aLon) * Math.PI) / 180;\n const sa = Math.sin(dLat / 2);\n const sb = Math.sin(dLon / 2);\n const x = sa * sa + Math.cos((aLat * Math.PI) / 180) * Math.cos((bLat * Math.PI) / 180) * sb * sb;\n return 2 * R * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));\n}\n\nasync function checkPublishTargetHealth(protocol, target) {\n const p = String(protocol || \"\").toUpperCase();\n const t = String(target || \"\").trim();\n if (!t) return { ok: false, reason: \"target_missing\", shouldReconnect: false };\n if (p === \"RTMP\") {\n const ok = t.startsWith(\"rtmp://\") || t.startsWith(\"rtmps://\");\n return { ok, reason: ok ? \"rtmp_target_format_ok\" : \"invalid_rtmp_target\", shouldReconnect: !ok, reconnectInSec: 15 };\n }\n if (p === \"WHIP\" || p === \"SFU\") {\n try {\n const res = await fetch(t, { method: \"OPTIONS\" });\n const ok = res.status < 500;\n return { ok, reason: `http_${res.status}`, shouldReconnect: !ok, reconnectInSec: 8 };\n } catch {\n return { ok: false, reason: \"network_unreachable\", shouldReconnect: true, reconnectInSec: 8 };\n }\n }\n return { ok: true, reason: \"unchecked_protocol\", shouldReconnect: false };\n}\n\nasync function fetchTextFromUrl(url) {\n const res = await fetch(url, { method: \"GET\" });\n if (!res.ok) throw new Error(`fetch_${res.status}`);\n const ct = String(res.headers.get(\"content-type\") || \"\");\n if (ct.includes(\"application/pdf\")) throw new Error(\"pdf_binary_not_supported_directly\");\n const body = await res.text();\n return stripHtml(body).slice(0, 14000);\n}\n\nfunction buildTaskActionGraph(task = \"\", context = {}) {\n const t = String(task || \"\").toLowerCase();\n if (t.includes(\"dinner\") || t.includes(\"akşam yemeği\") || t.includes(\"aksam yemegi\")) {\n return {\n taskTitle: \"Dinner Prep Planner\",\n nodes: [\n { id: \"n1\", title: \"Kitchen preheat\", action: \"smart-oven.preheat\", params: { temperatureC: 190, mode: \"fan\" } },\n { id: \"n2\", title: \"Lighting comfort\", action: \"smart-light.set_scene\", params: { scene: \"warm_dinner\" } },\n { id: \"n3\", title: \"Fridge inventory check\", action: \"smart-fridge.inventory_scan\", params: {} },\n { id: \"n4\", title: \"Reminder notification\", action: \"smart-watch.notify\", params: { text: \"Dinner prep started.\" } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" },\n { from: \"n3\", to: \"n4\" }\n ]\n };\n }\n return {\n taskTitle: task || \"General Planner\",\n nodes: [\n { id: \"n1\", title: \"Safety check\", action: \"device.safety_check\", params: { context } },\n { id: \"n2\", title: \"Primary action\", action: \"device.execute_task\", params: { task } },\n { id: \"n3\", title: \"Report\", action: \"studio.timeline_note\", params: { task } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" }\n ]\n };\n}\n\nfunction buildEthicsPrograms() {\n return {\n policyVersion: \"v1\",\n tracks: [\n {\n id: \"elderly-care-assist\",\n audience: \"elderly\",\n title: \"Elderly Assistance Track\",\n principles: [\"human-in-the-loop\", \"consent-first\", \"low-friction alerts\"],\n modules: [\"medication-reminder\", \"fall-risk-check\", \"family-notify\", \"daily-companion-brief\"]\n },\n {\n id: \"kids-safe-learning\",\n audience: \"children\",\n title: \"Kids Safe Learning Track\",\n principles: [\"guardian-consent\", \"age-appropriate-language\", \"content-filtering\"],\n modules: [\"study-coach\", \"screen-time-balance\", \"safe-social-share\", \"emergency-contact\"]\n }\n ]\n };\n}\n\nasync function deliverViaTelegram(channel, text) {\n const chatId = String(channel.endpoint || \"\").trim();\n if (!TELEGRAM_BOT_TOKEN) throw new Error(\"telegram_token_missing\");\n if (!chatId) throw new Error(\"telegram_chat_id_missing\");\n const url = `https://api.telegram.org/bot${encodeURIComponent(TELEGRAM_BOT_TOKEN)}/sendMessage`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ chat_id: chatId, text })\n });\n if (!res.ok) throw new Error(`telegram_http_${res.status}`);\n return { provider: \"telegram\", status: res.status };\n}\n\nasync function deliverViaWhatsapp(channel, text) {\n const to = String(channel.endpoint || \"\").trim();\n if (!WHATSAPP_TOKEN) throw new Error(\"whatsapp_token_missing\");\n if (!WHATSAPP_PHONE_NUMBER_ID) throw new Error(\"whatsapp_phone_number_id_missing\");\n if (!to) throw new Error(\"whatsapp_to_missing\");\n const url = `https://graph.facebook.com/v22.0/${encodeURIComponent(WHATSAPP_PHONE_NUMBER_ID)}/messages`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${WHATSAPP_TOKEN}` },\n body: JSON.stringify({ messaging_product: \"whatsapp\", to, type: \"text\", text: { body: text } })\n });\n if (!res.ok) throw new Error(`whatsapp_http_${res.status}`);\n return { provider: \"whatsapp\", status: res.status };\n}\n\nasync function deliverViaWebhook(channel, text) {\n const endpoint = String(channel.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"webhook_endpoint_missing\");\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text, platform: channel.platform || \"webhook\", ts: Date.now() })\n });\n if (!res.ok) throw new Error(`webhook_http_${res.status}`);\n return { provider: \"webhook\", status: res.status };\n}\n\nasync function deliverSocialMessage(channel, text) {\n const platform = String(channel.platform || \"webhook\").toLowerCase();\n if (platform === \"telegram\") return deliverViaTelegram(channel, text);\n if (platform === \"whatsapp\") return deliverViaWhatsapp(channel, text);\n return deliverViaWebhook(channel, text);\n}\n\nasync function deliverSocialWithRetry(channel, text) {\n let attempt = 0;\n let lastError = null;\n while (attempt <= SOCIAL_RETRY_MAX) {\n try {\n const result = await deliverSocialMessage(channel, text);\n return { ok: true, attempt: attempt + 1, ...result };\n } catch (error) {\n lastError = error;\n if (attempt >= SOCIAL_RETRY_MAX) break;\n const jitter = Math.floor(Math.random() * 90);\n await sleep(SOCIAL_RETRY_BASE_MS * Math.pow(2, attempt) + jitter);\n }\n attempt += 1;\n }\n return { ok: false, attempt: attempt + 1, error: String(lastError?.message || \"delivery_failed\") };\n}\n\nasync function dispatchCommandToAdapter(device, command) {\n const adapter = String(command?.adapter || device?.adapter || \"websocket\").toLowerCase();\n const endpoint = String(device?.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"device_endpoint_missing\");\n if (adapter === \"mqtt\") {\n // MQTT delivery is proxied via a bridge endpoint (HTTP webhook style).\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n type: \"mqtt-command\",\n topic: `castle/devices/${device.id}/commands`,\n action: command.action,\n params: command.params || {}\n })\n });\n if (!res.ok) throw new Error(`mqtt_bridge_http_${res.status}`);\n return { ok: true, adapter, via: \"http-bridge\", status: res.status };\n }\n if (!endpoint.startsWith(\"ws://\") && !endpoint.startsWith(\"wss://\")) throw new Error(\"invalid_websocket_endpoint\");\n const sendResult = await new Promise((resolve, reject) => {\n const ws = new WebSocket(endpoint);\n const timer = setTimeout(() => {\n try {\n ws.close();\n } catch {\n /* noop */\n }\n reject(new Error(\"websocket_timeout\"));\n }, 3000);\n ws.on(\"open\", () => {\n ws.send(\n JSON.stringify({\n type: \"castle-command\",\n commandId: command.id,\n deviceId: command.deviceId,\n action: command.action,\n params: command.params || {}\n }),\n (err) => {\n clearTimeout(timer);\n ws.close();\n if (err) reject(new Error(\"websocket_send_failed\"));\n else resolve({ ok: true, adapter: \"websocket\", via: \"ws\" });\n }\n );\n });\n ws.on(\"error\", () => {\n clearTimeout(timer);\n reject(new Error(\"websocket_connect_failed\"));\n });\n });\n return sendResult;\n}\n\nfunction readHttpJson(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => {\n if (!buf) return resolve({});\n const payload = safeJsonParse(buf);\n if (!payload || typeof payload !== \"object\") return reject(new Error(\"invalid_json_payload\"));\n resolve(payload);\n });\n req.on(\"error\", reject);\n });\n}\n\n/** Ham gövde (HMAC doğrulaması için). */\nfunction readHttpBodyText(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => resolve(buf));\n req.on(\"error\", reject);\n });\n}\n\nfunction maskOpaqueId(value) {\n const t = String(value || \"\");\n if (!t) return \"(empty)\";\n if (t.length <= 8) return \"****\";\n return `${t.slice(0, 4)}…${t.slice(-2)}`;\n}\n\nfunction readGatewayHttpToken(req) {\n const x = String(req.headers?.[\"x-castle-gateway-token\"] || \"\").trim();\n if (x) return x;\n const auth = String(req.headers?.authorization || \"\");\n const m = /^Bearer\\s+(.+)$/i.exec(auth);\n return m ? String(m[1] || \"\").trim() : \"\";\n}\n\nasync function resolveHttpUser(req) {\n const authResult = await verifyClientToken(req);\n if (authResult?.ok && authResult.user?.uid) {\n return { ok: true, uid: String(authResult.user.uid), mode: authResult.kind || \"auth\" };\n }\n if (ALLOW_DEV_ANON && ALLOW_DEV_HTTP_UID) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) return { ok: true, uid: `dev-${devUid.slice(0, 64)}`, mode: \"dev-header\" };\n }\n return { ok: false, reason: authResult?.reason || \"auth_required\" };\n}\n\n/**\n * Epistemic ledger ingest — Firebase uid OR valid transport token + X-Castle-Dev-Uid.\n * Prod disables CASTLE_ALLOW_DEV_HTTP_UID; LLM may still work while telemetry 401'd without this.\n */\nasync function resolveEpistemicIngestActor(req) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok && tok === REQUIRED_GATEWAY_TOKEN) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) {\n return { ok: true, uid: `gw-telemetry-${devUid.slice(0, 64)}`, mode: \"gateway-token-dev-uid\" };\n }\n return { ok: false, reason: \"dev_uid_required\" };\n }\n if (tok) {\n return { ok: false, reason: \"gateway_token_mismatch\" };\n }\n }\n const auth = await resolveHttpUser(req);\n if (auth.ok) return auth;\n return { ok: false, reason: auth.reason || \"auth_required\" };\n}\n\n/** Mesh actor: Firebase uid, dev header, or `guest:<uuid>` via header / `guestId` query (SSE cannot set headers). */\nasync function resolveMeshActor(req) {\n const auth = await resolveHttpUser(req);\n if (auth.ok) return { ok: true, clientUid: auth.uid, mode: auth.mode };\n if (ALLOW_MESH_GUEST) {\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const gid = String(req.headers?.[\"x-castle-guest-id\"] || q.searchParams.get(\"guestId\") || \"\").trim();\n if (gid) {\n const normalized = gid.startsWith(\"guest:\") ? gid.slice(0, 180) : `guest:${gid.slice(0, 160)}`;\n return { ok: true, clientUid: normalized, mode: \"guest-header\" };\n }\n }\n return { ok: false, reason: \"mesh_auth_required\" };\n}\n\n/** Live genesis runtime JSON (GET /rhizoh/genesis/runtime + SSE infra sampler). */\nasync function buildGenesisRuntimeSurfacePayloadLive() {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n let spiralWsActive = 0;\n if (spiralWssForGenesis) {\n for (const c of spiralWssForGenesis.clients) {\n if (c.readyState === 1) spiralWsActive += 1;\n }\n }\n const base = buildGenesisRuntimeSurfacePayload({\n infraMetrics,\n scoredHealth: scored,\n rhizohEnterpriseMetrics,\n mesh: meshContinuityAggregate(),\n workerHealth: worker,\n port: PORT,\n spiralWebSocketClientsActive: spiralWsActive\n });\n return {\n ...base,\n replayFingerprint: computeGenesisReplayFingerprintV0(base),\n gatewayCapabilities: buildGenesisGatewayCapabilitiesV0(),\n genesisStream: {\n schema: \"castle.genesis.stream_cursor.v0\",\n lastAcceptedSeq: getGenesisContinuitySeq(),\n clientIngress: {\n schema: GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n contract: \"castle.world_observation.ingress_envelope.v1\",\n postPath: rhizohRuntime.routes.genesisIngress,\n closure: getGenesisWorldObservationIngressStatusV0()\n },\n seqContinuity: getGenesisContinuitySeqAuditV0(),\n eventArchive: genesisContinuityEventArchiveEnabled()\n ? {\n enabled: true,\n projection: GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION,\n queryPath: rhizohRuntime.routes.genesisContinuityEvents\n }\n : { enabled: false },\n replayRouter: {\n schema: GENESIS_REPLAY_ROUTER_SCHEMA,\n queryPath: rhizohRuntime.routes.genesisReplay,\n queryHint: \"from=&to= or range=from-to; optional type=&checkpoints=0\",\n temporalDiffPath: rhizohRuntime.routes.genesisReplayDiff,\n temporalDiffSchema: GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA,\n equivalencePath: rhizohRuntime.routes.genesisReplayEquivalence,\n analyticsPath: rhizohRuntime.routes.genesisReplayAnalytics,\n evolutionPath: rhizohRuntime.routes.genesisReplayEvolution\n }\n }\n };\n}\n\nconst httpServer = createServer(async (req, res) => {\n const corsAllow = applyHttpCorsHeaders(req, res);\n if (req.method === \"OPTIONS\") {\n if (!corsAllow && req.headers?.origin) {\n console.warn(\"[GATEWAY_CORS] preflight rejected for origin:\", req.headers.origin);\n }\n res.writeHead(corsAllow ? 204 : 403);\n res.end();\n return;\n }\n\n const pathname = getHttpPathname(req);\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/genesis/__ping\") {\n sendJson(res, 200, {\n ok: true,\n pid: process.pid,\n entry: __filename,\n time: Date.now(),\n genesis: true\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/epistemic/__probe\") {\n sendJson(res, 200, {\n ok: true,\n epistemicIngestActorV2: true,\n routes: {\n seal: rhizohRuntime.routes.epistemicSeal,\n logsBatch: rhizohRuntime.routes.epistemicLogsBatch,\n authorityLedgerBatch: rhizohRuntime.routes.authorityLedgerBatch,\n authorityLedgerReplay: rhizohRuntime.routes.authorityLedgerReplay,\n authorityEpochMerge: rhizohRuntime.routes.authorityEpochMerge\n },\n hasGatewayToken: Boolean(REQUIRED_GATEWAY_TOKEN),\n gatewayTokenLen: REQUIRED_GATEWAY_TOKEN ? REQUIRED_GATEWAY_TOKEN.length : 0\n });\n return;\n }\n\n if (\n req.method === \"GET\" &&\n (pathname === \"/rhizoh/live/world-feed\" ||\n pathname === \"/rhizoh/live/sports-bundle\" ||\n pathname === \"/rhizoh/live/news-headlines\")\n ) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_live_world_feed:${ip}`, RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const out =\n pathname === \"/rhizoh/live/sports-bundle\"\n ? await handleLiveSportsBundleGetV0()\n : pathname === \"/rhizoh/live/news-headlines\"\n ? await handleLiveNewsHeadlinesGetV0(req)\n : await handleLiveWorldFeedGetV0(req);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/webhooks/stripe\") {\n await handleStripeMembershipWebhook(req, res);\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/billing/stripe/checkout\") {\n await handleStripeCheckoutCreate(req, res, { readHttpJson, resolveHttpUser, sendJson });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/live\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, live: false, dns: true, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n sendJson(res, 200, { ok: true, live: true, dns: true, service: \"castle-gateway\", ts: Date.now() });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/cors\") {\n const reqOrigin = normalizeHttpOrigin(req.headers?.origin);\n const allowOrigin = accessControlAllowOriginValue(req);\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n requestOrigin: reqOrigin || null,\n allowed: Boolean(allowOrigin),\n allowOrigin: allowOrigin || null,\n allowSetSize: httpCors.allowSet.size\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/ready\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, ready: false, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n try {\n const persistence = getFirebasePersistence();\n const llmProbe = rhizohLlmEnvConfigured();\n sendJson(res, 200, {\n ok: true,\n ready: true,\n dns: true,\n persistence: persistence.mode,\n service: \"castle-gateway\",\n gateway: \"ok\",\n llm: llmProbe ? \"ok\" : \"degraded\",\n spine: llmProbe ? \"ok\" : \"partial\"\n });\n } catch (e) {\n sendJson(res, 503, { ok: false, ready: false, reason: String(e?.message || e), service: \"castle-gateway\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/reality\") {\n const payload = buildRealityHealthPayload();\n sendJson(res, payload.ok ? 200 : 503, payload);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/substrate\") {\n const authority = buildGatewaySubstrateAuthoritySnapshotV0();\n const operational = buildSubstrateOperationalSnapshotV0();\n const drift = buildRealityHealthPayload();\n const ok = Boolean(authority.productionAuthoritySatisfied) && drift.ok;\n sendJson(res, ok ? 200 : 503, {\n ok,\n service: \"castle-gateway\",\n ts: Date.now(),\n authority,\n operational,\n drift: { ok: drift.ok, errors: drift.drift?.errors, warnings: drift.drift?.warnings }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.councilAnomalyReasoning) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_council_anomaly:${ip}`, 30, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const body = await readHttpJson(req);\n const out = handleCouncilAnomalyReasoningPostV0(body);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/substrate/health\") {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const body = await readHttpJson(req);\n const uid = (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestClientSubstrateHealthV0(body, { uid, clientId: uid });\n if (!result.ok) {\n sendJson(res, 400, result);\n return;\n }\n recordSubstrateOtelEventV0(\"client_health_ingest\", { uid: String(uid).slice(0, 32) });\n sendJson(res, 200, result);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/deps\") {\n const t0 = Date.now();\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, {\n ok: false,\n dns: true,\n llm: false,\n firestore: false,\n phase: \"maintenance\",\n latencyMs: Date.now() - t0,\n service: \"castle-gateway\"\n });\n return;\n }\n const persistence = getFirebasePersistence();\n let firestoreOk = persistence.mode !== \"firebase\";\n let firestoreDetail = persistence.mode === \"file\" ? \"file_mode\" : \"not_applicable\";\n if (persistence.mode === \"firebase\" && persistence.db) {\n const pr = await probeFirestoreReachable(persistence.db, 2000);\n firestoreOk = pr.ok;\n firestoreDetail = pr.reason || (pr.ok ? \"reachable\" : \"fail\");\n } else if (persistence.mode === \"firebase\" && !persistence.db) {\n firestoreOk = false;\n firestoreDetail = \"firebase_not_initialized\";\n }\n const llmOk = rhizohLlmEnvConfigured();\n const overallOk = llmOk && firestoreOk;\n const latencyMs = Date.now() - t0;\n sendJson(res, 200, {\n ok: overallOk,\n dns: true,\n llm: llmOk,\n firestore: persistence.mode === \"firebase\" ? firestoreOk : false,\n persistence: persistence.mode,\n firestoreDetail,\n latencyMs,\n service: \"castle-gateway\",\n wsPort: PORT\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health\") {\n const persistence = getFirebasePersistence();\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n wsPort: PORT,\n persistence: persistence.mode,\n presenceMesh: \"sse\"\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/health\") {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n sendJson(res, 200, {\n ok: true,\n role: \"gateway\",\n queueLagMs: infraMetrics.queueLag,\n queueDepth: infraMetrics.queueDepth,\n errors: infraMetrics.errors,\n status: scored.status,\n score: scored.score,\n reasons: scored.reasons,\n worker: worker\n ? {\n status: worker.status,\n score: worker.score,\n reasons: worker.reasons\n }\n : null,\n timestamp: Date.now()\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisRuntime) {\n const payload = await buildGenesisRuntimeSurfacePayloadLive();\n sendJson(res, 200, payload);\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.genesisIngress) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const actor = await resolveMeshActor(req);\n const clientId = actor.ok ? actor.clientUid : (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestGenesisWorldObservationV0(body, { clientId });\n if (!result.ok) {\n const status =\n result.error === \"rate_limited\" || result.error === \"agent_spoke_throttled\"\n ? 429\n : result.error === \"payload_too_large\"\n ? 413\n : 400;\n sendJson(res, status, result);\n return;\n }\n sendJson(res, result.idempotent ? 200 : 202, result);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisStream) {\n const corsOrigin = accessControlAllowOriginValue(req);\n if (!corsOrigin && String(process.env.NODE_ENV || \"\").toLowerCase() === \"production\") {\n const og = String(req.headers?.origin || \"\").trim();\n if (og) {\n console.warn(\"[GATEWAY] genesis SSE: missing Access-Control-Allow-Origin for browser Origin (CORS whitelist mismatch):\", og);\n }\n }\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n res.write(\"retry: 3000\\n\\n\");\n res.write(\": genesis continuity stream\\n\\n\");\n res.write(\"event: genesis\\n\");\n res.write(`data: {\"booted\":true,\"pid\":${process.pid}}\\n\\n`);\n registerGenesisSseClient(res, req);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLatest) {\n const cp = getLatestGenesisSignedCheckpoint();\n if (!cp) {\n sendJson(res, 404, { ok: false, error: \"no_checkpoint_yet\", hint: \"wait_for_seq_interval_or_configure_signing_secret\" });\n return;\n }\n sendJson(res, 200, { ok: true, checkpoint: cp });\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(rhizohRuntime.routes.genesisCheckpointBySeqPrefix)) {\n const tail = pathname.slice(rhizohRuntime.routes.genesisCheckpointBySeqPrefix.length).split(\"/\")[0];\n const seq = parseInt(String(tail || \"\").trim(), 10);\n const out = await genesisCheckpointQueryBySeqV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"checkpoint_not_found\") {\n sendJson(res, 404, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointRange) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryRangeV0(from, to);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"range_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLineage) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const seq = parseInt(String(u.searchParams.get(\"seq\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryLineageV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"lineage_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisContinuityEvents) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const limit = parseInt(String(u.searchParams.get(\"limit\") || \"\").trim(), 10);\n const out = await queryGenesisContinuityEventArchiveV0(from, to, type, limit);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\" || out.error === \"event_archive_disabled\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"archive_file_too_large\") {\n sendJson(res, 413, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplay) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await resolveGenesisReplayRouterV1({\n from: fromSeq,\n to: toSeq,\n type,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayDiff) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const out = await computeGenesisReplayTemporalDiffV1({ from: fromSeq, to: toSeq, type });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEquivalence) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from1 = parseInt(String(u.searchParams.get(\"from1\") || \"\").trim(), 10);\n const to1 = parseInt(String(u.searchParams.get(\"to1\") || \"\").trim(), 10);\n const from2 = parseInt(String(u.searchParams.get(\"from2\") || \"\").trim(), 10);\n const to2 = parseInt(String(u.searchParams.get(\"to2\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await compareGenesisReplayEquivalenceV1({\n from1,\n to1,\n from2,\n to2,\n type,\n includeCheckpoints\n });\n if (\n !out.ok &&\n (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")\n ) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayAnalytics) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayAnalyticsV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEvolution) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const collapseWindows = parseInt(String(u.searchParams.get(\"collapseWindows\") || \"0\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayEvolutionV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n collapseWindows,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/metrics\") {\n res.writeHead(200, { \"Content-Type\": \"text/plain; version=0.0.4; charset=utf-8\" });\n res.end(renderInfraMetricsProm());\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.infraPrometheusMetrics) {\n if (process.env.CASTLE_PROMETHEUS_METRICS !== \"1\") {\n sendJson(res, 404, { ok: false, error: \"prometheus_metrics_disabled\" });\n return;\n }\n res.writeHead(200, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(renderRhizohPrometheusMetrics());\n return;\n }\n if (req.method === \"GET\" && pathname.startsWith(\"/infra/traces/\")) {\n const traceId = decodeURIComponent(pathname.slice(\"/infra/traces/\".length));\n const item = await getTraceById(traceId);\n if (!item) return sendJson(res, 404, { ok: false, error: \"trace_not_found\" });\n return sendJson(res, 200, { ok: true, trace: item });\n }\n if (req.method === \"GET\" && pathname === \"/infra/traces\") {\n const u = new URL(String(req.url || \"/infra/traces\"), `http://${req.headers.host || \"localhost\"}`);\n const sessionId = String(u.searchParams.get(\"sessionId\") || \"\").trim();\n const items = sessionId ? await listTracesBySession(sessionId) : [];\n return sendJson(res, 200, { ok: true, sessionId, traces: items });\n }\n\n /** Presence room mesh (C): JOIN / LEAVE / SNAPSHOT / DELTA / REPLAY (HTTP); SUBSCRIBE = SSE stream. */\n if (req.method === \"POST\" && pathname === \"/presence/mesh/join\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid, clientUid: actor.clientUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/leave\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshLeave(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/snapshot\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n sendJson(res, 200, meshSnapshot(roomUid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/delta\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, MAX_MESSAGE_BYTES);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const seq = meshAppendDelta(roomUid, actor.clientUid, {\n node: body?.node,\n projectionPatch: body?.projectionPatch,\n writerSubject: body?.writerSubject\n });\n sendJson(res, 200, { ok: true, roomUid, seq });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/replay\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const fromNodeId = body?.fromNodeId != null ? String(body.fromNodeId) : \"\";\n const fromSeq = body?.fromSeq;\n if (fromNodeId) {\n sendJson(res, 200, meshReplayFromNodeId(roomUid, fromNodeId));\n return;\n }\n sendJson(res, 200, meshReplayFromSeq(roomUid, fromSeq));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/presence/mesh/subscribe\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const roomUid = String(q.searchParams.get(\"roomUid\") || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n const write = (chunk) => {\n try {\n res.write(chunk);\n } catch {\n /* closed */\n }\n };\n const unsub = meshSubscribeSse(roomUid, write);\n const onClose = () => {\n unsub();\n meshLeave(roomUid, actor.clientUid);\n try {\n res.end();\n } catch {\n /* ignore */\n }\n };\n req.on(\"close\", onClose);\n req.on(\"aborted\", onClose);\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, uid: auth.uid, mode: auth.mode, items: await listConnections(auth.uid) });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const id = await createConnection(auth.uid, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"create_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n await updateConnection(auth.uid, id, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"update_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"DELETE\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n await deleteConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"delete_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url?.startsWith(\"/rhizoh/network/presence\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const u = new URL(req.url, \"http://localhost\");\n const roomKey = String(u.searchParams.get(\"room\") || \"world_space_c2c_v0\").slice(0, 64);\n const sessionId = String(u.searchParams.get(\"sessionId\") || \"\").trim();\n const unified = listUnifiedGatewayPresenceV0({ roomKey, sessionId: sessionId || undefined });\n sendJson(res, 200, {\n ok: true,\n roomKey,\n sessionId: sessionId || null,\n presence: unified,\n castlePeers: unified.castlePeers,\n matchMembers: unified.matchMembers,\n services: unified.services\n });\n } catch (error) {\n sendJson(res, 500, { ok: false, error: error?.message || \"presence_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/rhizoh/sync/vault\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const snapshot = await getCloudSyncSnapshotV0(auth.uid);\n sendJson(res, 200, { ok: true, uid: auth.uid, snapshot });\n } catch (error) {\n sendJson(res, 500, { ok: false, error: error?.message || \"cloud_sync_pull_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/rhizoh/sync/vault\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const snapshot = await mergeCloudSyncSnapshotV0(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, uid: auth.uid, snapshot });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"cloud_sync_push_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections/test\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n if (!checkHttpRateLimit(`llm_conn_test:${auth.uid}`, RL_LLM_CONN_TEST_PER_MIN, 60_000)) {\n return sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n }\n const hasBodyKey = !!String(payload?.apiKey || \"\").trim();\n const result = await queryRhizohLlm(\n {\n message: String(payload?.message || \"Ping from Castle Gateway\"),\n context: payload?.context || { source: \"connection_test\" },\n provider: payload?.provider,\n model: payload?.model,\n apiKey: payload?.apiKey || \"\"\n },\n { allowExternalApiKey: true, llmKeySource: hasBodyKey ? \"auto\" : \"env\" }\n );\n logLlmAccess({\n route: \"/llm/connections/test\",\n uid: auth.uid,\n llmKeyBillingOwner: result.llmKeyBillingOwner,\n llmKeyOrigin: result.llmKeyOrigin,\n provider: result.provider,\n model: result.model,\n connectionId: null\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"connection_test_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAgentIdentities(auth.uid);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerAgentIdentity({\n ownerUid: auth.uid,\n agentId: payload?.agentId,\n role: payload?.role,\n personaSeed: payload?.personaSeed || {},\n capabilityLevel: payload?.capabilityLevel\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/agents/identities/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const owns = await getAgentIdentity(id);\n if (!owns || owns.ownerUid !== auth.uid) return sendJson(res, 403, { ok: false, error: \"forbidden\" });\n const payload = await readHttpJson(req);\n const row = await updateAgentIdentity(id, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/academy/events\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/academy/events\"}`);\n const status = String(url.searchParams.get(\"status\") || \"\");\n const items = await listAcademyEvents({ ownerUid: auth.uid, status, limit: 120 });\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/academy/events\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const ev = await queueAcademyEvent({\n type: payload?.type,\n roomId: payload?.roomId,\n ownerUid: auth.uid,\n participants: payload?.participants || [],\n topic: payload?.topic || \"academy\",\n payload: payload?.payload || {}\n });\n sendJson(res, 200, { ok: true, event: ev });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && String(req.url || \"\").startsWith(\"/academy/events/\") && String(req.url || \"\").endsWith(\"/resolve\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const parts = String(req.url || \"\").split(\"/\").filter(Boolean);\n const eventId = parts[2] || \"\";\n try {\n const payload = await readHttpJson(req);\n const out = await resolveAcademyEvent(eventId, payload || {});\n sendJson(res, 200, { ok: true, event: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_resolve_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/memory\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/memory\"}`);\n const scope = url.searchParams.get(\"scope\") === \"agents\" ? \"agents\" : \"users\";\n const id = String(url.searchParams.get(\"id\") || (scope === \"agents\" ? \"\" : auth.uid));\n const kind = String(url.searchParams.get(\"kind\") || \"\");\n const limit = Number(url.searchParams.get(\"limit\") || 40);\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n sendJson(res, 200, { ok: true, scope, id, kind, items: await listMemories({ scope, id, kind, limit }) });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const profile = await getPersonaGoalMemory(auth.uid);\n sendJson(res, 200, { ok: true, profile });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const profile = await setPersonaGoalMemory({\n uid: auth.uid,\n personaPatch: payload?.personaPatch || {},\n goals: Array.isArray(payload?.goals) ? payload.goals : null,\n preferencesPatch: payload?.preferencesPatch || {}\n });\n sendJson(res, 200, { ok: true, profile });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"profile_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/compact\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const out = await autoCompactMemories({ scope, id });\n sendJson(res, 200, { ok: true, scope, id, ...out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_compact_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/context\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const context = await getMemoryContext({\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n query: String(payload?.query || \"\"),\n limit: Number(payload?.limit || 80)\n });\n sendJson(res, 200, { ok: true, context });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_context_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/place-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const placeName = String(payload?.placeName || \"\").trim();\n if (!placeName) return sendJson(res, 400, { ok: false, error: \"placeName_required\" });\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const persona = String(payload?.waypointPersona || payload?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(placeName)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Place: ${placeName}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Give a concise conversational briefing about this place for a walking-route companion.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-place-brief\", placeName, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `PLACE_BRIEF:${placeName} => ${String(summary.reply || \"\").slice(0, 900)}`,\n tags: [\"place\", \"brief\", \"event-layer\"],\n importance: 0.62,\n meta: { placeName }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"place-brief\",\n text: `${placeName} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: {\n placeName,\n directive: summary.directive,\n lat: hit?.lat ? Number(hit.lat) : null,\n lon: hit?.lon ? Number(hit.lon) : null\n }\n });\n sendJson(res, 200, { ok: true, place: hit || null, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"place_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/route-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const waypoints = Array.isArray(payload?.waypoints) ? payload.waypoints.slice(0, 24) : [];\n const out = [];\n for (const wp of waypoints) {\n const name = String(wp?.name || \"\").trim();\n if (!name) continue;\n const persona = String(wp?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(name)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Waypoint: ${name}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Generate a concise spoken briefing for route waypoint.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-route-brief\", waypoint: name, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n out.push({\n name,\n persona,\n place: hit || null,\n reply: summary.reply,\n directive: summary.directive\n });\n }\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `ROUTE_BRIEF: waypoints=${out.length} ${out.map((x) => `${x.name}:${x.persona}`).join(\" | \")}`.slice(0, 1300),\n tags: [\"route\", \"waypoint\", \"brief\", \"event-layer\"],\n importance: 0.74,\n meta: { count: out.length }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"route-brief\",\n text: out.map((x) => `${x.name}:${String(x.reply || \"\").slice(0, 140)}`).join(\" | \").slice(0, 1400),\n roomId: \"studio-main\",\n meta: {\n waypointCount: out.length,\n waypoints: out.map((x) => ({\n name: x.name,\n persona: x.persona,\n lat: x.place?.lat ? Number(x.place.lat) : null,\n lon: x.place?.lon ? Number(x.place.lon) : null\n }))\n }\n });\n sendJson(res, 200, { ok: true, items: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"route_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/pdf-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const title = String(payload?.title || \"document\");\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const providedText = String(payload?.text || \"\");\n const url = String(payload?.url || \"\");\n let content = providedText;\n if (!content && url) content = await fetchTextFromUrl(url);\n if (!content) return sendJson(res, 400, { ok: false, error: \"text_or_supported_url_required\" });\n const summary = await queryRhizohLlm(\n {\n message: `Summarize this document and provide key points + practical actions.\\nTitle:${title}\\nContent:\\n${content.slice(0, 12000)}`,\n context: { source: \"event-layer-pdf-brief\", title },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `DOC_BRIEF:${title} => ${String(summary.reply || \"\").slice(0, 1000)}`,\n tags: [\"document\", \"brief\", \"library\", \"event-layer\"],\n importance: 0.7,\n meta: { title, url: url || null }\n });\n await addTranscript(auth.uid, {\n source: \"library-brief\",\n eventType: \"pdf-brief\",\n text: `${title} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { title }\n });\n sendJson(res, 200, { ok: true, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/pdf/upload\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req, 8 * 1024 * 1024);\n const job = await enqueuePdfJob({\n uid: auth.uid,\n fileName: payload?.fileName || \"document.pdf\",\n bytesBase64: payload?.contentBase64\n });\n sendJson(res, 200, { ok: true, job });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_upload_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/session\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createPublishSession(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session\",\n text: `Publish session armed: ${row.protocol} ${row.target}`.slice(0, 500),\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: row.id, bridge: row.bridge }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/studio/publish/sessions\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listPublishSessions(auth.uid, 80);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/health-check\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const protocol = String(payload?.protocol || \"WHIP\");\n const target = String(payload?.target || \"\");\n const health = await checkPublishTargetHealth(protocol, target);\n if (payload?.sessionId) {\n await updatePublishSession(auth.uid, String(payload.sessionId), {\n status: health.ok ? \"healthy\" : \"degraded\",\n reconnectPolicy: {\n enabled: true,\n backoffSec: [2, 5, 10, 20],\n nextRetryInSec: health.reconnectInSec || 0,\n maxRetries: 8,\n reason: health.reason\n }\n });\n }\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-health\",\n text: `${protocol} ${target} => ${health.ok ? \"healthy\" : \"degraded\"} (${health.reason})`,\n roomId: \"studio-main\",\n meta: { protocol, target, reconnect: !!health.shouldReconnect }\n });\n sendJson(res, 200, { ok: true, health });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_health_check_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/studio/publish/sessions/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updatePublishSession(auth.uid, id, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session-update\",\n text: `Session ${id} -> ${row.status || \"updated\"}`,\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: id, status: row.status, ingestUrl: row.ingestUrl || \"\" }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/transcripts\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/transcripts\"}`);\n const limit = Math.max(1, Math.min(200, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await listTranscripts(auth.uid, limit);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/transcripts\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await addTranscript(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"transcript_write_failed\" });\n }\n return;\n }\n\n /** E1: broadcast intent → GreenRoom capability → transcript + memory artifact */\n if (req.method === \"POST\" && req.url === \"/studio/capabilities/greenroom\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const traceId = String(payload?.traceId || `TRC-${Date.now().toString(36).toUpperCase()}`);\n const title = String(payload?.title || \"Castle Live Broadcast\").slice(0, 200);\n const intentRaw = String(payload?.intentRaw || \"\").slice(0, 800);\n const audienceEstimate = Math.max(0, Math.min(50_000_000, Number(payload?.audienceEstimate) || 1200));\n const roomId = String(payload?.roomId || \"greenroom-main\");\n const transcriptRow = await addTranscript(auth.uid, {\n source: \"rhizoh\",\n eventType: \"BROADCAST_ROUTED\",\n text: `GreenRoom · ${title}${intentRaw ? `\\nIntent: ${intentRaw}` : \"\"}`,\n roomId,\n meta: {\n ack: \"BROADCAST_ROUTED\",\n capability: \"greenroom\",\n intentType: \"broadcast\",\n traceId,\n audienceEstimate,\n artifact: {\n kind: \"broadcast_card\",\n title,\n traceId,\n roomId,\n createdAt: Date.now()\n }\n }\n });\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n text: `GREENROOM_BROADCAST:${title} trace=${traceId}`,\n tags: [\"broadcast\", \"greenroom\", \"artifact\", \"castle-library\"],\n importance: 0.72,\n kind: \"episodic\",\n meta: { traceId, capability: \"greenroom\", transcriptId: transcriptRow.id }\n });\n sendJson(res, 200, {\n ok: true,\n ack: \"BROADCAST_ROUTED\",\n traceId,\n transcript: transcriptRow,\n replayPath: `/replay/${encodeURIComponent(traceId)}`,\n sharePath: `/replay/${encodeURIComponent(traceId)}`\n });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"greenroom_capability_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/metadata/search\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/metadata/search\"}`);\n const q = String(url.searchParams.get(\"q\") || \"\");\n const limit = Math.max(1, Math.min(300, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await searchMetadata(auth.uid, q, limit);\n sendJson(res, 200, { ok: true, q, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerDevice(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, device: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"device_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const lat = Number(payload?.lat);\n const lon = Number(payload?.lon);\n const speed = Math.max(0, Number(payload?.speed || 0));\n const cfgCenterLat = Number(process.env.CASTLE_GEOFENCE_CENTER_LAT || 41.0082);\n const cfgCenterLon = Number(process.env.CASTLE_GEOFENCE_CENTER_LON || 28.9784);\n const cfgRadiusM = Math.max(100, Number(process.env.CASTLE_GEOFENCE_RADIUS_M || 55000));\n const cfgSpeedCap = Math.max(1, Number(process.env.CASTLE_SPEED_CAP_MPS || 35));\n const dist = Number.isFinite(lat) && Number.isFinite(lon) ? haversineMeters(cfgCenterLat, cfgCenterLon, lat, lon) : 0;\n const geofenceOk = !Number.isFinite(dist) ? true : dist <= cfgRadiusM;\n const speedOk = speed <= cfgSpeedCap;\n const failsafe = !(geofenceOk && speedOk);\n const safety = {\n geofenceOk,\n speedOk,\n failsafe,\n distanceFromCenterM: Number.isFinite(dist) ? dist : null,\n speedCapMps: cfgSpeedCap\n };\n const row = await appendTelemetry(auth.uid, deviceId, {\n lat,\n lon,\n speed,\n battery: payload?.battery,\n mode: payload?.mode || (failsafe ? \"FAILSAFE_HOLD\" : \"ACTIVE\"),\n raw: payload?.raw || {}\n });\n await updateDevice(auth.uid, deviceId, {\n status: failsafe ? \"failsafe\" : \"active\",\n lastTelemetryAt: row.ts,\n safetyState: safety\n });\n if (failsafe) {\n await addTranscript(auth.uid, {\n source: \"robotics-guardrails\",\n eventType: \"failsafe\",\n text: `Device ${deviceId} failsafe triggered geofenceOk=${geofenceOk} speedOk=${speedOk}`,\n roomId: \"studio-main\",\n meta: { deviceId, safety }\n });\n }\n sendJson(res, 200, { ok: true, telemetry: row, safety, command: failsafe ? \"FAILSAFE_HOLD\" : \"CONTINUE\" });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"telemetry_ingest_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listTelemetry(auth.uid, 150);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createAutomation(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"automation-scheduled\",\n text: `Automation scheduled: ${row.title} at ${new Date(row.scheduleAt).toLocaleString()}`,\n roomId: \"studio-main\",\n meta: { automationId: row.id, action: row.action, deviceId: row.deviceId }\n });\n sendJson(res, 200, { ok: true, automation: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"automation_create_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAutomations(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listDevices(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const devs = await listDevices(auth.uid, 500);\n const d = devs.find((x) => x.id === deviceId);\n if (!d) return sendJson(res, 404, { ok: false, error: \"device_not_found\" });\n const action = String(payload?.action || \"\");\n const caps = Array.isArray(d.capabilityProfile) ? d.capabilityProfile : [];\n if (action && caps.length && !caps.includes(action) && !caps.includes(\"*\")) {\n return sendJson(res, 400, { ok: false, error: \"unsupported_action_for_device\", capabilityProfile: caps });\n }\n const cmd = await enqueueCommand(auth.uid, {\n deviceId,\n action: action || \"NOOP\",\n params: payload?.params || {},\n adapter: payload?.adapter || d.adapter || \"websocket\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-command-queue\",\n eventType: \"command-queued\",\n text: `${cmd.deviceId} -> ${cmd.action}`,\n roomId: \"studio-main\",\n meta: { commandId: cmd.id, adapter: cmd.adapter }\n });\n sendJson(res, 200, { ok: true, command: cmd });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listCommands(auth.uid, 200);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/robotics/commands/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updateCommandStatus(auth.uid, id, payload?.status || \"sent\");\n sendJson(res, 200, { ok: true, command: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/rhizoh/bridge\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const result = await runRhizohBrainV2({\n message: String(payload?.message || \"robotics bridge command\"),\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n worldState: payload?.worldState || {},\n deviceState: payload?.deviceState || {},\n intent: \"robotics-mechanics\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-bridge\",\n eventType: \"robotics-command\",\n text: `Bridge -> ${String(result.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { directive: result.directive, agentId: payload?.agentId || \"\" }\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"robotics_bridge_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/planner/graph\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const graph = buildTaskActionGraph(String(payload?.task || \"\"), payload?.context || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"planner-graph\",\n text: `Planner graph built: ${graph.taskTitle} nodes=${graph.nodes.length}`,\n roomId: \"studio-main\",\n meta: { taskTitle: graph.taskTitle, nodeCount: graph.nodes.length }\n });\n sendJson(res, 200, { ok: true, graph });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"planner_graph_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerSocialChannel(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, channel: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_channel_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listSocialChannels(auth.uid, 100);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/broadcast\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const text = String(payload?.text || \"\").slice(0, 1200);\n const channels = await listSocialChannels(auth.uid, 100);\n const enabled = channels.filter((c) => c.status === \"enabled\");\n const deliveries = [];\n for (const c of enabled) {\n const out = await deliverSocialWithRetry(c, text);\n deliveries.push({ id: c.id, platform: c.platform, ...out });\n }\n const okCount = deliveries.filter((d) => d.ok).length;\n await addTranscript(auth.uid, {\n source: \"social-layer\",\n eventType: \"social-broadcast\",\n text: `Broadcast delivered ${okCount}/${enabled.length} channel(s): ${text.slice(0, 240)}`,\n roomId: \"studio-main\",\n meta: { deliveries }\n });\n sendJson(res, 200, { ok: true, sentChannels: okCount, totalChannels: enabled.length, deliveries });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_broadcast_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/ethics/programs\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, ...buildEthicsPrograms() });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands/dispatch\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const all = await listCommands(auth.uid, 200);\n const queued = all.filter((c) => String(c.status || \"queued\") === \"queued\").slice(0, 50);\n const devices = await listDevices(auth.uid, 500);\n const byId = new Map(devices.map((d) => [d.id, d]));\n const results = [];\n for (const cmd of queued) {\n const dev = byId.get(cmd.deviceId);\n if (!dev) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: \"device_not_found\" });\n continue;\n }\n try {\n const dispatched = await dispatchCommandToAdapter(dev, cmd);\n await updateCommandStatus(auth.uid, cmd.id, \"sent\");\n results.push({ id: cmd.id, ok: true, adapter: dispatched.adapter, via: dispatched.via });\n } catch (error) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: String(error?.message || \"dispatch_failed\") });\n }\n }\n await addTranscript(auth.uid, {\n source: \"robotics-command-dispatch\",\n eventType: \"command-dispatch-batch\",\n text: `Dispatch batch size=${results.length} ok=${results.filter((x) => x.ok).length}`,\n roomId: \"studio-main\",\n meta: { results: results.slice(0, 30) }\n });\n sendJson(res, 200, { ok: true, processed: results.length, results });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"dispatch_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/pdf/jobs/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n const job = await getPdfJob(auth.uid, id);\n if (!job) return sendJson(res, 404, { ok: false, error: \"pdf_job_not_found\" });\n sendJson(res, 200, { ok: true, job });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const row = await appendMemory({\n scope,\n id,\n text: payload?.text,\n tags: payload?.tags || [],\n importance: payload?.importance,\n kind: payload?.kind || \"episodic\",\n meta: { uid: auth.uid, ...(payload?.meta || {}) }\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.externalTruth) {\n try {\n if (process.env.RHIZOH_EXTERNAL_TRUTH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_truth:${ip}`, RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n sendJson(res, 200, buildRhizohExternalGroundTruthPayload(req));\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.productOutcome) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const rawBody = await readHttpBodyText(req, 64 * 1024);\n const body = rawBody.trim() ? safeJsonParse(rawBody) : null;\n if (!body || typeof body !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"invalid_json_payload\" });\n return;\n }\n const sigHdr = String(req.headers?.[\"x-rhizoh-outcome-signature\"] || \"\").trim();\n const hmacV = verifyRhizohOutcomeHmac(rawBody, sigHdr);\n if (!hmacV.ok) {\n sendJson(res, 401, { ok: false, error: hmacV.error || \"outcome_hmac_invalid\" });\n return;\n }\n const srcTokHdr = String(req.headers?.[\"x-rhizoh-outcome-source-token\"] || \"\").trim();\n const srcV = verifyRhizohOutcomeSourceToken(body.source, srcTokHdr);\n if (!srcV.ok) {\n sendJson(res, 401, { ok: false, error: srcV.error || \"outcome_source_token_invalid\" });\n return;\n }\n const subjectKey = String(body.subjectKey || \"\").trim().slice(0, 128);\n if (\n subjectKey &&\n !checkHttpRateLimit(\n `rhizoh_product_outcome_subj:${subjectKey}`,\n RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN,\n 60_000\n )\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_subject\" });\n return;\n }\n const result = ingestRhizohProductOutcomeHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n schemaVersion: \"1.1.0\",\n aggregate: result.aggregate,\n integrity: result.integrity\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"outcome_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.productOutcomeAggregate) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome_agg:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const u = new URL(String(req.url || \"/\"), \"http://localhost\");\n const cohortId = u.searchParams.get(\"cohortId\") || \"\";\n const decisionFingerprint = u.searchParams.get(\"decisionFingerprint\") || \"\";\n const limit = u.searchParams.get(\"limit\") || \"50\";\n const rows = listRhizohProductOutcomeAggregates({\n cohortId: cohortId || undefined,\n decisionFingerprint: decisionFingerprint || undefined,\n limit: Number(limit) || 50\n });\n sendJson(res, 200, { ok: true, schemaVersion: \"1.1.0\", rows });\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.externalLossBatch) {\n try {\n if (process.env.RHIZOH_EXTERNAL_LOSS_BATCH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_loss_batch:${ip}`, RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const result = ingestRhizohExternalLossBatchHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, { ok: true, accepted: result.accepted ?? 0 });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"batch_failed\") });\n }\n return;\n }\n\n function moderationAdminOk(req) {\n const expected = String(process.env.CASTLE_MODERATION_ADMIN_KEY || \"\").trim();\n if (!expected) return false;\n const got = String(req.headers[\"x-castle-moderation-key\"] || \"\").trim();\n return got.length > 0 && got === expected;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.opsAbuseReport) {\n try {\n const body = await readHttpJson(req, 32 * 1024);\n const auth = await resolveHttpUser(req);\n const entry = submitAbuseReportV0({\n reporterUid: auth.ok ? auth.uid : null,\n targetUid: body?.targetUid ?? null,\n category: body?.category,\n detail: body?.detail,\n traceId: body?.traceId ?? null\n });\n sendJson(res, 202, { ok: true, reportId: entry.id, status: entry.status });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsModerationQueue) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n queue: listModerationQueueV0(100),\n softBlocked: listSoftBlockedUsersV0()\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-block\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const out = softBlockUserV0(body?.uid, body?.reason);\n sendJson(res, out.ok ? 200 : 400, out);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-unblock\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n sendJson(res, 200, removeSoftBlockV0(body?.uid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsAgentSnapshots) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n const limit = Number(new URL(req.url || \"\", \"http://local\").searchParams.get(\"limit\") || 50);\n sendJson(res, 200, { ok: true, snapshots: listRecentAgentSnapshotsV0(limit) });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.academicObservatoryExport) {\n const auth = await resolveHttpUser(req);\n const out = handleAcademicObservatoryExportGetV0(req, auth);\n sendJson(res, out.status, out.body);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsHardeningStatus) {\n const auth = await resolveHttpUser(req);\n const principal = auth.ok ? `uid:${auth.uid}` : null;\n const gclSnap = await getGlobalCostLedgerSnapshotV0(principal);\n const economicStrategy = evaluateEconomicStrategyL3V0(gclSnap);\n const hardeningTenantId = resolveHardeningTenantIdV0(auth, req);\n const unifiedState = await buildUnifiedStateNarrativeV0({\n tenantId: hardeningTenantId ?? undefined,\n platformScope: !hardeningTenantId,\n principal\n });\n sendJson(res, 200, {\n ok: true,\n schema: \"rhizoh.ops.hardening_status.v0\",\n containment: readAgentContainmentConfigV0(),\n cost: readCostContainmentConfigV0(),\n gcl: {\n backend: await getGclBackendStatusV0(),\n snapshot: gclSnap\n },\n economicStrategyL3: economicStrategy,\n phasedRollout: await getPhasedRolloutStatsV0(),\n tier: readPhasedRolloutTierV0(),\n principalCost: auth.ok ? await getCostContainmentStatsV0(principal) : null,\n stressTaxonomy: verifyStressTaxonomyCoverageV0(),\n stressConflictResolution: verifyStressConflictResolutionV0(),\n resolutionStabilityEnvelope: verifyResolutionStabilityEnvelopeV0(),\n unifiedState: {\n tenantScope: {\n tenantId: unifiedState.tenantScope?.tenantId,\n scope: unifiedState.tenantScope?.scope,\n noGlobalDerivedState: unifiedState.tenantScope?.noGlobalDerivedState\n },\n narrativeFingerprint: unifiedState.narrativeFingerprint,\n screenshotScopeWatermark: unifiedState.screenshotScopeWatermark,\n interpretationSafetyContract: unifiedState.interpretationSafetyContract,\n interpretationUxContract: {\n schema: unifiedState.interpretationUxContract?.schema,\n layerOrder: unifiedState.interpretationUxContract?.layerOrder,\n narrativeNeverVisuallyDominant: unifiedState.interpretationUxContract?.narrativeNeverVisuallyDominant\n },\n governance: {\n narrativeIsNotDecisionLayer: unifiedState.governance.narrativeIsNotDecisionLayer,\n displayOrder: unifiedState.governance.displayOrder,\n trustPosture: unifiedState.governance.trustPosture,\n interpretationUxContract: unifiedState.governance.interpretationUxContract?.schema\n },\n humanOps: {\n decisionTier: unifiedState.humanOps?.humanDecisionScaling?.tier,\n decisionOwner: unifiedState.humanOps?.humanDecisionScaling?.decisionOwner,\n opsRouteId: unifiedState.humanOps?.humanDecisionOpsRunbook?.routing?.routeId,\n slaAckMinutes: unifiedState.humanOps?.humanDecisionOpsRunbook?.sla?.ackMinutes,\n uiDisclaimerRequired: unifiedState.humanOps?.narrativeUiSafety?.displayRules?.requireDisclaimerBanner,\n misreadHighResidual: unifiedState.humanOps?.misreadSimulation?.highResidualCount,\n propagationHighResidual: unifiedState.humanOps?.socialPropagationSimulation?.highResidualCount,\n worstWatermarkSurvivability:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.worstWatermarkSurvivability,\n dominantDistortionSource:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.dominantDistortionSource\n },\n culturalRisk: {\n perceptualRiskClass: unifiedState.culturalRisk?.perceptualRiskClass,\n trustFork: unifiedState.culturalRisk?.trustDynamics?.fork,\n trustDecayScore: unifiedState.culturalRisk?.trustDynamics?.scores?.trustDecay,\n mythologyScore: unifiedState.culturalRisk?.trustDynamics?.scores?.mythology,\n confidenceDetachmentExposure:\n unifiedState.culturalRisk?.trustDynamics?.axes?.confidenceDetachmentExposure\n },\n appliedSystemsLayer: {\n schema: unifiedState.appliedSystemsLayer?.schema,\n researchOutputType: unifiedState.appliedSystemsLayer?.researchGovernance?.outputType,\n roboticsBlockActuation: unifiedState.appliedSystemsLayer?.roboticsGrounding?.blockActuation,\n spiralRumorCount:\n unifiedState.appliedSystemsLayer?.spiralMMOGameKernel?.gameLayer?.rumors?.length\n },\n actionSemanticGovernance: {\n schema: unifiedState.actionSemanticGovernance?.schema,\n highRiskActionCount: unifiedState.actionSemanticGovernance?.highRiskActionCount,\n loopClosed: unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.loopClosed,\n authorityCollapseMitigated:\n unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.risks?.authorityCollapse\n ?.mitigated\n },\n actionContextResolution: {\n schema: unifiedState.actionContextResolution?.schema,\n contextFingerprint: unifiedState.actionContextResolution?.contextFingerprint?.fingerprintHash,\n domainCollision: unifiedState.actionContextResolution?.domainCollision?.detected,\n deployAgentSpiralBlocked:\n unifiedState.actionContextResolution?.actionOverloadingRisk?.deployAgentExample\n ?.spiral_mmo === \"blocked\",\n roboticsActuationEligible:\n unifiedState.actionContextResolution?.roboticsActuationCorrectness?.actuationEligible\n },\n epistemicCoherence: {\n schema: unifiedState.epistemicCoherence?.schema,\n verdict: unifiedState.epistemicCoherence?.systemCoherence?.verdict,\n coherenceScore: unifiedState.epistemicCoherence?.systemCoherence?.score,\n contradictionCount: unifiedState.epistemicCoherence?.crossLayerContradictions?.count,\n operatorDecision: unifiedState.epistemicCoherence?.uxCompression?.operatorDecision,\n headlineTr: unifiedState.epistemicCoherence?.uxCompression?.headline?.tr,\n falseSafetyWarning: Boolean(\n unifiedState.epistemicCoherence?.uxCompression?.falseSafetyWarning\n )\n },\n coherenceAuthorityBoundary: {\n schema: unifiedState.coherenceAuthorityBoundary?.schema,\n bindingValid: unifiedState.coherenceAuthorityBoundary?.eclAuthorityBinding?.valid,\n canExecute: unifiedState.coherenceAuthorityBoundary?.contract?.can_execute,\n mandatoryBannerTr:\n unifiedState.coherenceAuthorityBoundary?.disclaimers?.mandatoryBanner?.tr\n },\n decisionLatencyGovernance: {\n schema: unifiedState.decisionLatencyGovernance?.schema,\n latencyTier: unifiedState.decisionLatencyGovernance?.latencyTier,\n inflationScore: unifiedState.decisionLatencyGovernance?.latencyInflation?.score,\n fastPathEligible: unifiedState.decisionLatencyGovernance?.fastPath?.eligible,\n primaryActionId: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.primaryActionId,\n ackSlaMinutes: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.ackSlaMinutes,\n certaintyCap: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.certaintyCap,\n uncertaintyItems:\n unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.uncertaintyEnvelope?.itemCount\n },\n decisionPacketUncertaintyBoundary: {\n schema: unifiedState.decisionPacketUncertaintyBoundary?.schema,\n invariantValid: unifiedState.decisionPacketUncertaintyBoundary?.invariantCheck?.valid,\n centralInvariant: unifiedState.decisionPacketUncertaintyBoundary?.centralInvariant\n },\n epistemicDecisionPacing: {\n schema: unifiedState.epistemicDecisionPacing?.schema,\n queueSaturationTier:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation?.tier,\n operatorProcessingLatency:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation\n ?.operatorProcessingLatency,\n postureWindowCount:\n unifiedState.epistemicDecisionPacing?.temporaryStaticPostureWindows\n ?.temporaryStaticPostureWindows?.windowCount,\n executionBoundariesValid:\n unifiedState.epistemicDecisionPacing?.executionBoundaries?.valid,\n grantsExecutionApproval: unifiedState.epistemicDecisionPacing?.grantsExecutionApproval\n },\n epistemicTemporalCoherence: {\n schema: unifiedState.epistemicTemporalCoherence?.schema,\n allInvariantsValid: unifiedState.epistemicTemporalCoherence?.allInvariantsValid,\n temporalAlignmentValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.temporalAlignment?.valid,\n crossWindowGuardValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard?.valid,\n delayedTruthValid: unifiedState.epistemicTemporalCoherence?.invariants?.delayedTruthRule?.valid,\n reconciliationRequired:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard\n ?.reconciliation?.required\n },\n realityDriftObserver: {\n schema: unifiedState.realityDriftObserver?.schema,\n driftActive: unifiedState.realityDriftObserver?.driftObserverSummary?.active,\n misapprehensionShapeCount:\n unifiedState.realityDriftObserver?.misapprehensionShapeCatalog?.shapeCount,\n criticalShapeCount:\n unifiedState.realityDriftObserver?.driftObserverSummary?.criticalShapeCount,\n roboticsMismatchActive:\n unifiedState.realityDriftObserver?.roboticsFeedbackMismatch?.active\n },\n driftCausality: {\n schema: unifiedState.driftCausality?.schema,\n shapesWithCausalExplanation: unifiedState.driftCausality?.rdolLinkage?.shapesWithCausalExplanation,\n activeDomainCount: unifiedState.driftCausality?.causalitySummary?.activeDomainCount,\n propagationCausalityActive: unifiedState.driftCausality?.causalDomains?.propagation?.active\n },\n productReadiness: unifiedState.productReadiness,\n stateLayers: {\n raw: unifiedState.stateLayers.raw,\n derived: {\n systemState: unifiedState.stateLayers.derived.systemState,\n narrative: unifiedState.stateLayers.derived.narrative,\n validation: unifiedState.stateLayers.derived.validation\n },\n policy: {\n binding: unifiedState.stateLayers.policy.binding,\n decisionOwner: unifiedState.stateLayers.policy.decisionOwner,\n prohibitedActions: unifiedState.stateLayers.policy.prohibitedActions\n }\n },\n systemState: unifiedState.systemState,\n interpretation: unifiedState.interpretation,\n validation: {\n trustPosture: unifiedState.validation.trustPosture,\n adjustedConfidence: unifiedState.validation.adjustedConfidence,\n agreement: unifiedState.validation.confidenceDecomposition.agreement,\n divergenceScore: unifiedState.validation.divergence.divergenceScore,\n validated: unifiedState.validation.divergence.validated,\n uncertaintyTags: unifiedState.validation.uncertainty.tags.map((t) => t.id),\n flags: unifiedState.validation.divergence.flags.map((f) => f.id)\n },\n compression: unifiedState.compression\n }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 6 * 1024 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (\n !checkHttpRateLimit(`rhizoh_voice_transcribe:${rlKey}`, RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN, 60_000)\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const voiceEnv = rhizohVoiceTranscribeEnvV3();\n if (!voiceEnv.any) {\n sendJson(res, 503, {\n ok: false,\n error: \"voice_asr_not_configured\",\n env: voiceEnv\n });\n return;\n }\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n const result = await runRhizohVoiceTranscribeV3(\n applyLanguagePropagationToVoiceBodyV1(payload, langProp)\n );\n sendJson(res, result.ok ? 200 : 422, {\n ...result,\n traceId: String(payload?.traceId || \"\"),\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n atMs: Date.now()\n });\n } catch (error) {\n sendJson(res, 500, {\n ok: false,\n error: \"voice_transcribe_failed\",\n detail: String(error?.message || error),\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(\"/rhizoh/llm/task/\")) {\n try {\n const rawTaskId = decodeURIComponent(pathname.slice(\"/rhizoh/llm/task/\".length).split(\"/\")[0] || \"\");\n const taskId = String(rawTaskId || \"\").toLowerCase() === \"last\" ? getLatestLlmWorkerTaskIdV0() : rawTaskId;\n const snap = taskId ? getLlmWorkerTaskSnapshotV0(taskId) : null;\n if (!snap) {\n sendJson(res, 404, {\n ok: false,\n error: \"llm_task_not_found\",\n taskId: rawTaskId,\n status: \"missing\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n if (snap.status === \"processing\") {\n sendJson(res, 202, {\n ok: true,\n taskId: snap.taskId,\n status: \"processing\",\n createdAt: snap.createdAt,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n // Terminal states (completed/failed) always HTTP 200 — poll must not look like server fault (500).\n sendJson(res, 200, {\n ...(snap.body || { ok: false, error: \"llm_task_empty\", taskId: snap.taskId }),\n taskId: snap.taskId,\n status: snap.status === \"completed\" ? \"completed\" : snap.status,\n terminal: true,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n } catch (error) {\n console.error(\"[llm-task-poll] handler failed:\", error);\n sendJson(res, 200, {\n ok: false,\n error: \"llm_task_poll_handler_failed\",\n detail: String(error?.message || error),\n status: \"failed\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === rhizohRuntime.routes.rhizohLlm) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 128 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n logRhizohHealth(\"gateway_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: String(payload?.traceId || \"\"),\n auth: auth.ok ? \"ok\" : \"anon\",\n ip\n });\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (!checkHttpRateLimit(`rhizoh_llm:${rlKey}`, RL_RHIZOH_LLM_PER_MIN, 60_000)) {\n const rlTaxonomy = classifyStressResponseV0({ code: \"rate_limit_exceeded\" });\n return sendJson(res, 429, {\n ok: false,\n error: \"rate_limit_exceeded\",\n reply: rlTaxonomy.userMessageTr || \"İstek sınırı aşıldı. Kısa süre sonra tekrar deneyin.\",\n stressClass: rlTaxonomy.stressClass,\n responseAction: rlTaxonomy.responseAction,\n stressMatrix: rlTaxonomy.matrix,\n stressInterpretable: rlTaxonomy.interpretable,\n stressConfidence: rlTaxonomy.stressConfidence,\n actionConfidence: rlTaxonomy.actionConfidence,\n actionSoftened: rlTaxonomy.actionSoftened,\n responseActionStrict: rlTaxonomy.responseActionStrict,\n actionInterpretable: rlTaxonomy.actionInterpretable,\n conflictResolution: rlTaxonomy.conflictResolution,\n directive: \"NONE\"\n });\n }\n\n const requireExplicit = process.env.CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE === \"1\";\n const keyMode = normalizeClientLlmKeySource(payload?.llmKeySource ?? payload?.keySource);\n if (keyMode === null) {\n return sendJson(res, 400, {\n ok: false,\n error: \"invalid_llm_key_source\",\n reply: \"llmKeySource geçersiz. Kullanın: env | user_connection | auto\",\n directive: \"NONE\"\n });\n }\n if (requireExplicit && keyMode === \"auto\") {\n return sendJson(res, 400, {\n ok: false,\n error: \"llm_key_source_required\",\n reply: \"Sunucu CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE=1: istekte llmKeySource (env veya user_connection) zorunlu.\",\n directive: \"NONE\"\n });\n }\n\n const conn = auth.ok ? await resolveConnection(auth.uid, String(payload?.connectionId || \"\")) : null;\n\n if (keyMode === \"user_connection\") {\n if (!auth.ok) {\n return sendJson(res, 401, {\n ok: false,\n error: \"llm_auth_required_for_user_connection\",\n reply: \"Kayıtlı LLM bağlantısı için giriş gerekli.\",\n directive: \"NONE\"\n });\n }\n if (!conn?.apiKey) {\n return sendJson(res, 400, {\n ok: false,\n error: \"user_llm_connection_required\",\n reply: \"Geçerli bir LLM bağlantısı yok; Studio’da bağlantı ekleyin veya llmKeySource=env kullanın.\",\n directive: \"NONE\"\n });\n }\n }\n\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n let safePayload = applyLanguagePropagationToLlmPayloadV1({ ...(payload || {}) }, langProp);\n const trimOut = trimRhizohLlmGatewayPayloadV1(safePayload);\n if (trimOut.meta.trimmed) {\n logRhizohHealth(\"llm_payload_trimmed\", {\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n bytesBefore: trimOut.meta.bytesBefore,\n bytesAfter: trimOut.meta.bytesAfter\n });\n }\n safePayload = trimOut.payload;\n delete safePayload.apiKey;\n delete safePayload.llmKeySource;\n delete safePayload.keySource;\n\n const llmMessage = String(\n safePayload?.message || safePayload?.text || payload?.message || payload?.text || \"\"\n ).trim();\n if (!llmMessage) {\n return sendJson(res, 400, {\n ok: false,\n error: \"message_required\",\n reply: \"Mesaj metni gerekli (message alanı boş).\",\n directive: \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n if (!safePayload.message) {\n safePayload = { ...safePayload, message: llmMessage };\n }\n\n let resolvedProvider = payload?.provider;\n let resolvedModel = payload?.model;\n let connApiKey = \"\";\n\n ({\n resolvedProvider,\n resolvedModel,\n connApiKey\n } = resolveServerLlmProviderV0(keyMode, conn, payload));\n\n const turnInput = {\n safePayload,\n auth,\n keyMode,\n conn,\n resolvedProvider,\n resolvedModel,\n connApiKey\n };\n const turnMeta = {\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n langEcho: buildRhizohLanguagePropagationEchoV1(langProp)\n };\n\n if (\n isLlmWorkerEnabledV0() &&\n isLlmWorkerAsyncEnabledV0() &&\n String(req.headers[\"x-rhizoh-llm-sync\"] || \"\").trim() !== \"1\"\n ) {\n const taskId = enqueueLlmWorkerTaskAsyncV0(turnInput, turnMeta);\n logRhizohHealth(\"llm_worker_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n taskId,\n traceId: String(payload?.traceId || taskId),\n keyMode,\n async: true\n });\n sendJson(res, 202, {\n ok: true,\n taskId,\n status: \"processing\",\n traceId: String(payload?.traceId || taskId),\n pollPath: `/rhizoh/llm/task/${taskId}`,\n gatewayPid: process.pid,\n ...turnMeta.langEcho\n });\n return;\n }\n\n const { result, traceId, turnLatencyMs, spinePhases, sampledTrace } = isLlmWorkerEnabledV0()\n ? await runLlmWorkerTaskV0(turnInput, turnMeta)\n : await rhizohGatewayTurn(turnInput);\n logRhizohHealth(\"llm_response\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId,\n turnLatencyMs: Number(turnLatencyMs || 0),\n keyMode\n });\n sendJson(res, 200, {\n ...result,\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n traceId,\n turnLatencyMs,\n ...(spinePhases ? { spinePhases } : {}),\n ...(sampledTrace !== undefined ? { sampledTrace } : {})\n });\n } catch (error) {\n const msg = String(error?.message || \"\");\n const code = error?.code || \"\";\n let status = 500;\n if (msg === \"payload_too_large\") status = 413;\n else if (msg === \"rate_limit_exceeded\") status = 429;\n else if (\n code === \"cost_hard_limit\" ||\n code === \"phased_rollout_capacity\" ||\n error?.containment === true ||\n String(code).startsWith(\"agent_\")\n ) {\n status = 429;\n } else if (code === \"user_soft_blocked\" || code === \"prompt_abuse_detected\") status = 403;\n else if (\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(code) ||\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(msg) ||\n msg.includes(\"missing_api_key_for_\")\n )\n status = 400;\n let rhizohFailureKind = \"provider_error\";\n if (msg === \"rate_limit_exceeded\" || status === 429) rhizohFailureKind = \"rate_limit\";\n else if (status === 403) rhizohFailureKind = \"policy_block\";\n else if (status === 400 || status === 413) rhizohFailureKind = \"client_config\";\n else if (msg.startsWith(\"provider_http_\")) {\n const st = Number(msg.replace(\"provider_http_\", \"\"));\n if (st === 408 || st === 504) rhizohFailureKind = \"timeout\";\n else rhizohFailureKind = \"provider_error\";\n } else if (/timeout|timed out|ETIMEDOUT/i.test(msg)) rhizohFailureKind = \"timeout\";\n\n const providerHttpMatch = msg.match(/^provider_http_(\\d{3})$/);\n const stressTaxonomy =\n error?.stressClass != null\n ? {\n stressClass: error.stressClass,\n responseAction: error.responseAction,\n stressMatrix: error.stressMatrix,\n stressInterpretable: error.stressInterpretable !== false,\n stressConfidence: error.stressConfidence,\n actionConfidence: error.actionConfidence,\n actionSoftened: error.actionSoftened,\n responseActionStrict: error.responseActionStrict,\n actionInterpretable: error.actionInterpretable,\n conflictResolution: error.conflictResolution,\n stressSecondary: error.stressSecondary,\n responseActions: error.responseActions\n }\n : (() => {\n const t = classifyStressResponseV0({\n code: code || msg,\n rhizohFailureKind,\n providerHttpStatus: providerHttpMatch\n ? Number(providerHttpMatch[1])\n : null\n });\n return {\n stressClass: t.stressClass,\n responseAction: t.responseAction,\n stressMatrix: t.matrix,\n stressInterpretable: t.interpretable,\n stressConfidence: t.stressConfidence,\n actionConfidence: t.actionConfidence,\n actionSoftened: t.actionSoftened,\n responseActionStrict: t.responseActionStrict,\n actionInterpretable: t.actionInterpretable,\n conflictResolution: t.conflictResolution,\n stressSecondary: t.stressSecondary,\n responseActions: t.responseActions\n };\n })();\n sendJson(res, status, {\n ok: false,\n rhizohFailureKind,\n ...(providerHttpMatch ? { providerHttpStatus: Number(providerHttpMatch[1]) } : {}),\n error: code || msg || \"rhizoh_llm_failed\",\n detail: msg || code || undefined,\n reply:\n error?.reply ||\n (status === 500 ? \"Rhizoh bağlantısı geçici olarak kesildi.\" : msg),\n directive: error?.directive || \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n ...stressTaxonomy\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicSeal) {\n try {\n if (!EPISTEMIC_SEAL_SECRET) {\n sendJson(res, 503, { ok: false, error: \"epistemic_seal_disabled\" });\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`epistemic_seal:${ip}`, RL_EPISTEMIC_SEAL_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const truth = body.truth_contract;\n if (!truth || typeof truth !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"truth_contract_required\" });\n return;\n }\n const sealBody = {\n truth_contract: truth,\n runtime_hash: String(body.runtime_hash || \"\"),\n model_route: {\n provider: body.model_route?.provider ?? null,\n model: body.model_route?.model ?? null\n },\n memory_digest: String(body.memory_digest || \"\"),\n world_snapshot_hash: String(body.world_snapshot_hash || \"\"),\n timestamp: Number(body.timestamp) || Date.now()\n };\n const canonical = canonicalEpistemicSealString(sealBody);\n const { hash, signature } = hashAndSignEpistemicSeal(canonical, EPISTEMIC_SEAL_SECRET);\n recordGenesisEpistemicSealIssued(hash);\n const attestation = {\n schema: EPISTEMIC_SEAL_SCHEMA,\n algorithm: \"SHA-256+HMAC-SHA256\",\n keyHint: process.env.CASTLE_EPISTEMIC_SEAL_SECRET ? \"CASTLE_EPISTEMIC_SEAL_SECRET\" : \"CASTLE_GATEWAY_TOKEN\",\n issuedAt: Date.now(),\n gateway: { service: \"castle-gateway\", listenPort: PORT }\n };\n sendJson(res, 200, {\n ok: true,\n hash,\n signature,\n attestation,\n canonicalBytes: Buffer.byteLength(canonical, \"utf8\")\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"seal_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicLogsBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`epistemic_logs:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 120) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const persisted = await persistEpistemicLedgerBatch(auth.uid, entries);\n recordGenesisEpistemicLedgerPersisted(persisted.persisted);\n // Internal cognition layer: forecast stays gateway/ledger-only (no client exposure).\n try {\n await persistEpistemicForecastBatch(auth.uid, persisted.normalized || []);\n } catch {\n /* forecast failures must not break primary ledger ingest */\n }\n sendJson(res, 200, {\n ok: true,\n persisted: persisted.persisted,\n mode: persisted.mode,\n latest: persisted.normalized?.[persisted.normalized.length - 1] || null\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"epistemic_log_ingest_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.authorityLedgerBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`authority_ledger:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 80) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const witnessed = persistAuthorityLedgerWitnessBatchV1(auth.uid, entries, EPISTEMIC_SEAL_SECRET);\n if (witnessed.witnessed > 0) {\n recordGenesisEpistemicLedgerPersisted(witnessed.witnessed);\n const clientSeal = witnessed.lastWitness?.clientSealHash;\n if (clientSeal) recordGenesisEpistemicSealIssued(clientSeal);\n }\n sendJson(res, 200, {\n ok: true,\n witnessed: witnessed.witnessed,\n quarantined: witnessed.quarantined,\n chainHeight: witnessed.chainHeight,\n chainHead: witnessed.chainHead,\n lastWitness: witnessed.lastWitness,\n results: witnessed.results\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"authority_ledger_witness_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.authorityLedgerReplay) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const replayUrl = new URL(String(req.url || \"\"), \"http://localhost\");\n const epochId = String(replayUrl.searchParams.get(\"epochId\") || \"\").trim() || undefined;\n const snapshot = getAuthorityLedgerWitnessSnapshotV1(auth.uid, epochId);\n const replay = replayAuthorityLedgerWitnessChainV1(auth.uid, epochId);\n sendJson(res, 200, {\n ok: true,\n snapshot,\n replay,\n schema: \"castle.rhizoh.authority_ledger_witness.v1.replay_surface\"\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"authority_ledger_replay_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.authorityEpochMerge) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`authority_epoch_merge:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const merged = assimilateAuthorityEpochMergeV1(auth.uid, body);\n if (!merged.ok) {\n sendJson(res, 400, merged);\n return;\n }\n sendJson(res, 200, {\n ok: true,\n mergeEvent: merged.mergeEvent,\n recentMerges: merged.recentMerges\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"authority_epoch_merge_failed\") });\n }\n return;\n }\n\n sendJson(res, 404, { ok: false, error: \"not_found\" });\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\nspiralWssForGenesis = wss;\nconst orchestrator = createOrchestrator();\nconst clientStats = new Map();\nlet broadcasterClientId = null;\nconst spiralState = {\n rooms: [{ id: \"room-default\", name: \"Spiral Default Arena\" }],\n activeByClient: new Map(),\n characters: new Map() // id -> {id,name,role,roomId,source}\n};\n\n/** Castle social rooms — keyed by `castleRoomKey` from {@link WS_MESSAGE.CASTLE_SOCIAL_PULSE}. */\nconst castleSocialRoomState = new Map();\nconst CASTLE_SOCIAL_MEMBER_TTL_MS = 90_000;\nconst CASTLE_SOCIAL_MAX_PULSE_BYTES = 8192;\nconst RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES = 5 * 1024 * 1024;\nconst RHIZOH_VOICE_LIVE_MAX_CHUNKS = 96;\n\n/**\n * Voice Engine v3 live media lane.\n * Client streams MediaRecorder chunks over the existing gateway WS; STOP reuses the\n * gateway ASR runner so orchestration remains the single authority downstream.\n */\nconst rhizohVoiceLiveSessions = new WeakMap();\n\nfunction appendRhizohVoiceLiveEventV0(session, type, payloadRefSeed = \"\") {\n if (!session) return null;\n session.eventSeq = (Number(session.eventSeq) || 0) + 1;\n const event = createRhizohImmutableEventV0({\n type,\n sessionId: session.sessionId,\n traceId: session.traceId,\n actorSeed: session.sessionId || session.traceId,\n eventSeq: session.eventSeq,\n payloadRef: createRhizohPayloadRefV0(payloadRefSeed || `${session.sessionId}:${type}:${session.eventSeq}`)\n });\n session.events = [...(session.events || []), event].slice(-64);\n return event;\n}\n\nfunction sendRhizohVoiceLiveEnvelopeV0(socket, type, payload) {\n try {\n if (socket?.readyState === 1) socket.send(JSON.stringify(createEnvelope(type, payload)));\n } catch {\n /* noop */\n }\n}\n\nfunction decodeRhizohVoiceLiveChunkV0(payload = {}) {\n const b64 = String(payload.audioBase64 || \"\").trim();\n if (!b64) return { ok: false, error: \"audio_base64_required\" };\n try {\n const buf = Buffer.from(b64, \"base64\");\n if (!buf.length) return { ok: false, error: \"audio_decode_empty\" };\n return { ok: true, buffer: buf };\n } catch {\n return { ok: false, error: \"audio_base64_invalid\" };\n }\n}\n\nfunction handleRhizohVoiceLiveStartV0(socket, payload = {}) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const sessionId = String(p.sessionId || `live_${Date.now().toString(36)}`).slice(0, 96);\n rhizohVoiceLiveSessions.set(socket, {\n sessionId,\n traceId: String(p.traceId || \"\").slice(0, 128),\n mimeType: String(p.mimeType || \"audio/webm\").slice(0, 80),\n languageCode: String(p.languageCode || \"tr-TR\").slice(0, 32),\n path: String(p.path || \"both\").slice(0, 24),\n chunks: [],\n chunkIndexes: [],\n totalBytes: 0,\n nextChunkIndex: 1,\n eventSeq: 0,\n events: [],\n startedAtMs: Date.now()\n });\n appendRhizohVoiceLiveEventV0(rhizohVoiceLiveSessions.get(socket), \"VOICE_LIVE_SESSION_STARTED\", `${sessionId}:live_session`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_START, {\n ok: true,\n sessionId,\n schema: \"castle.rhizoh.voice_live.gateway_lane.v0\"\n });\n}\n\nfunction handleRhizohVoiceLiveChunkV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const sequence = validateRhizohVoiceLiveChunkPayloadV0(payload, {\n expectedChunkIndex: session.nextChunkIndex\n });\n if (!sequence.ok) {\n rhizohVoiceLiveSessions.delete(socket);\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:chunk_reject`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: sequence.error,\n expectedChunkIndex: sequence.expectedChunkIndex,\n chunkIndex: sequence.chunkIndex\n });\n return;\n }\n const decoded = decodeRhizohVoiceLiveChunkV0(payload);\n if (!decoded.ok) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: decoded.error\n });\n return;\n }\n if (\n session.chunks.length >= RHIZOH_VOICE_LIVE_MAX_CHUNKS ||\n session.totalBytes + decoded.buffer.length > RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES\n ) {\n rhizohVoiceLiveSessions.delete(socket);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_audio_too_large\"\n });\n return;\n }\n session.chunks.push(decoded.buffer);\n session.chunkIndexes.push(sequence.chunkIndex);\n session.totalBytes += decoded.buffer.length;\n session.nextChunkIndex += 1;\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_CHUNK_ACCEPTED\", `${session.sessionId}:chunk:${sequence.chunkIndex}`);\n}\n\nfunction handleRhizohVoiceLiveStopV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n rhizohVoiceLiveSessions.delete(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const lastChunkIndex = Number(payload?.lastChunkIndex);\n if (Number.isInteger(lastChunkIndex) && lastChunkIndex !== session.chunks.length) {\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:stop_mismatch`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_chunk_count_mismatch\",\n expectedChunkCount: lastChunkIndex,\n receivedChunkCount: session.chunks.length\n });\n return;\n }\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_STOP_RECEIVED\", `${session.sessionId}:stop:${session.chunks.length}`);\n const audio = Buffer.concat(session.chunks);\n if (!audio.length) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"audio_empty\"\n });\n return;\n }\n runRhizohVoiceTranscribeV3({\n path: String(payload?.path || session.path || \"both\"),\n audioBase64: audio.toString(\"base64\"),\n mimeType: String(payload?.mimeType || session.mimeType || \"audio/webm\"),\n languageCode: String(payload?.languageCode || session.languageCode || \"tr-TR\"),\n traceId: String(payload?.traceId || session.traceId || \"\"),\n sessionId: session.sessionId,\n transport: \"gateway_ws_live\"\n })\n .then((result) => {\n appendRhizohVoiceLiveEventV0(session, result?.ok ? \"VOICE_TRANSCRIBE_FINAL\" : \"VOICE_TRANSCRIBE_FAILED\", `${session.sessionId}:final`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_FINAL, {\n ...result,\n sessionId: session.sessionId,\n transportPath: \"gateway_ws_live\",\n bytes: audio.length,\n chunkCount: session.chunks.length,\n chunkSequence: Object.freeze({\n ok: true,\n firstChunkIndex: session.chunkIndexes[0] || null,\n lastChunkIndex: session.chunkIndexes[session.chunkIndexes.length - 1] || null,\n receivedChunkCount: session.chunks.length\n }),\n eventTrace: Object.freeze((session.events || []).slice(-16)),\n recordedMs: Math.max(0, Date.now() - session.startedAtMs)\n });\n })\n .catch((e) => {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_transcribe_failed\",\n detail: String(e?.message || e)\n });\n });\n}\n\nfunction removeCastleSocialClientFromAllRooms(clientId) {\n const cid = String(clientId || \"\");\n if (!cid) return;\n for (const room of castleSocialRoomState.values()) {\n room.members.delete(cid);\n }\n}\n\nfunction buildCastleSocialRoomPayload(roomKey) {\n const room = castleSocialRoomState.get(roomKey);\n if (!room) return null;\n const now = Date.now();\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const roster = [...room.members.values()].map((m) => ({\n userId: m.userId,\n lastMs: m.lastMs,\n ...m.sp\n }));\n const peerFeeds =\n room.walPeers && room.walPeers.size\n ? [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }))\n : [];\n return { castleRoomKey: roomKey, seq: room.seq, roster, peerFeeds, ts: now };\n}\n\nconst CASTLE_WAL_PEER_MAX_FEED_BYTES = 48 * 1024;\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {{ ok: boolean, code?: string, reason?: string }} gate\n */\nfunction rejectWalPeerIngressV0(socket, gate) {\n const code = gate.code || \"wal_peer_rejected\";\n recordWalPeerRejectV0(code);\n recordSubstrateOtelEventV0(\"wal_peer_reject\", { code, reason: String(gate.reason || \"\").slice(0, 120) });\n socket.send(\n JSON.stringify(\n createEnvelope(WS_MESSAGE.ERROR, {\n error: gate.reason || \"WAL peer ingress rejected.\",\n code\n })\n )\n );\n}\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {Record<string, unknown> | null | undefined} walPeerFeed\n * @returns {boolean}\n */\nfunction gateWalPeerIngressV0(socket, walPeerFeed) {\n const authGate = assertWalPeerSocketAuthorityV0(socket);\n if (!authGate.ok) {\n rejectWalPeerIngressV0(socket, authGate);\n return false;\n }\n const sigGate = validateWalPeerFeedSignatureV0(\n walPeerFeed && typeof walPeerFeed === \"object\" ? walPeerFeed : {}\n );\n if (!sigGate.ok) {\n rejectWalPeerIngressV0(socket, sigGate);\n return false;\n }\n return true;\n}\n\nfunction handleCastleWalPeerFeed(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_WAL_PEER_MAX_FEED_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_WAL_PEER_FEED too large.\" })));\n return;\n }\n const walPeerFeed = p.walPeerFeed && typeof p.walPeerFeed === \"object\" ? p.walPeerFeed : {};\n if (!gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const castleRoomKey = String(p.castleRoomKey || \"wal:convergence\").slice(0, 64);\n const castleId = String(p.castleId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId,\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: room.members.get(socket.clientId)?.sp ?? {}\n });\n const peerFeeds = [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }));\n const body = {\n castleRoomKey,\n seq: room.seq,\n peerFeeds,\n ts: now\n };\n const env = createEnvelope(WS_MESSAGE.CASTLE_WAL_PEER_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys(), ...room.walPeers.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction handleCastleSocialPulse(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_SOCIAL_MAX_PULSE_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_SOCIAL_PULSE too large.\" })));\n return;\n }\n const castleRoomKey = String(p.castleRoomKey || \"default\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n const sp = p.socialPulse && typeof p.socialPulse === \"object\" ? p.socialPulse : {};\n const walPeerFeed =\n p.walPeerFeed && typeof p.walPeerFeed === \"object\"\n ? p.walPeerFeed\n : sp.walPeerFeed && typeof sp.walPeerFeed === \"object\"\n ? sp.walPeerFeed\n : null;\n if (walPeerFeed && !gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const spNext = walPeerFeed ? { ...sp, walPeerFeed } : { ...sp };\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: spNext\n });\n if (walPeerFeed) {\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId: String(p.castleId || userId).slice(0, 64),\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n }\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const body = buildCastleSocialRoomPayload(castleRoomKey);\n if (!body) return;\n const env = createEnvelope(WS_MESSAGE.CASTLE_SOCIAL_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcast(msgObj) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcastState() {\n broadcast(\n createEnvelope(WS_MESSAGE.BROADCAST_STATE, {\n broadcasterClientId,\n viewers: [...wss.clients].filter((c) => c.readyState === 1).length\n })\n );\n}\n\nfunction currentSpiralStatePayload() {\n return {\n rooms: spiralState.rooms,\n characters: [...spiralState.characters.values()]\n };\n}\n\nfunction broadcastSpiralState() {\n broadcast(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload()));\n}\n\nfunction ensureRoom(roomId, fallbackName = null) {\n if (!roomId) return spiralState.rooms[0].id;\n const exists = spiralState.rooms.find((r) => r.id === roomId);\n if (exists) return exists.id;\n const created = { id: roomId, name: fallbackName || `Spiral-${roomId.slice(-4)}` };\n spiralState.rooms.push(created);\n return created.id;\n}\n\nfunction syncSpiralCharactersWithWorld(snapshot) {\n const agents = snapshot?.agents || [];\n for (const a of agents) {\n if (!spiralState.characters.has(a.id)) {\n spiralState.characters.set(a.id, {\n id: a.id,\n name: a.id,\n role: \"agent\",\n roomId: spiralState.rooms[0].id,\n source: \"world\"\n });\n }\n }\n}\n\nfunction broadcastExcept(msgObj, exceptSocket) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client !== exceptSocket && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction validateInputFrame(payload, socket) {\n const stats = clientStats.get(socket.clientId) || { lastFrame: 0, windowStart: Date.now(), msgCount: 0 };\n const now = Date.now();\n if (now - stats.windowStart > 5000) {\n stats.windowStart = now;\n stats.msgCount = 0;\n }\n stats.msgCount += 1;\n clientStats.set(socket.clientId, stats);\n if (stats.msgCount > 120) return \"Rate limit exceeded.\";\n\n if (!payload || !Number.isInteger(payload.frame)) return \"frame must be integer.\";\n if (payload.frame <= stats.lastFrame) return \"frame must be monotonic.\";\n const commands = Array.isArray(payload.commands) ? payload.commands : [];\n if (commands.length > 8) return \"Too many commands in one frame.\";\n for (const cmd of commands) {\n if (!cmd || typeof cmd.command !== \"string\") return \"Invalid command format.\";\n if (\n cmd.command !== \"SPAWN_AGENT\" &&\n cmd.command !== \"LIST_CASTLES\" &&\n cmd.command !== \"SPAWN_ENTITY\" &&\n cmd.command !== \"CREATE_CASTLE\"\n ) return \"Unsupported command.\";\n }\n stats.lastFrame = payload.frame;\n clientStats.set(socket.clientId, stats);\n return null;\n}\n\nfunction validateSignalPayload(payload) {\n if (!payload || typeof payload !== \"object\") return \"Invalid signaling payload.\";\n const signalType = payload.signalType;\n if (Object.values(CASTLE_NETWORK_SIGNAL_V0).includes(signalType)) {\n return null;\n }\n if (![\"OFFER\", \"ANSWER\", \"ICE\"].includes(signalType)) return \"Invalid signal type.\";\n if (typeof payload.to !== \"string\" || payload.to.length < 3) return \"Invalid signal destination.\";\n if (signalType !== \"ICE\" && JSON.stringify(payload.sdp || {}).length > 16 * 1024) return \"SDP too large.\";\n if (signalType === \"ICE\" && JSON.stringify(payload.candidate || {}).length > 6 * 1024) return \"ICE too large.\";\n return null;\n}\n\nfunction sanitizeRawMessage(raw) {\n if (!raw) return null;\n const text = raw.toString();\n if (Buffer.byteLength(text, \"utf8\") > MAX_MESSAGE_BYTES) return null;\n return text;\n}\n\nwss.on(\"connection\", async (socket, req) => {\n const origin = req?.headers?.origin || \"\";\n if (!httpCors.isWebSocketOriginAllowed(origin)) {\n socket.close(1008, \"Origin not allowed\");\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const path = req?.url || \"\";\n const query = new URL(`ws://localhost${path}`).searchParams;\n const token = query.get(\"token\");\n if (!token || token !== REQUIRED_GATEWAY_TOKEN) {\n socket.close(1008, \"Invalid gateway token\");\n return;\n }\n }\n if (REQUIRE_AUTH) {\n const result = await verifyClientToken(req);\n if (!result.ok) {\n const isLocal = origin.includes(\"localhost\") || origin.includes(\"127.0.0.1\") || !origin;\n if (!(ALLOW_DEV_ANON && isLocal)) {\n socket.close(1008, result.reason);\n return;\n }\n socket.auth = { ok: true, kind: \"dev-anon\", user: { uid: \"dev-anon\" } };\n } else {\n socket.auth = result;\n }\n }\n\n socket.clientId = `c-${Math.random().toString(36).slice(2, 8)}`;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.HELLO, { message: \"Castle Gateway online.\", clientId: socket.clientId })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.WORLD_SNAPSHOT, orchestrator.worldSnapshot())));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.BROADCAST_STATE, { broadcasterClientId, viewers: [...wss.clients].filter((c) => c.readyState === 1).length })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload())));\n broadcastExcept(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }), socket);\n\n socket.on(\"message\", (raw) => {\n const safeRaw = sanitizeRawMessage(raw);\n if (!safeRaw) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Message too large or invalid.\" })));\n return;\n }\n const parsed = safeJsonParse(safeRaw);\n if (!parsed?.type) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid message.\" })));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND_TEXT) {\n const canonical = parseTextToCommand(parsed.payload?.text);\n if (!canonical) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Unknown command text.\" })));\n return;\n }\n const result = orchestrator.applyCommand(canonical);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.MATCH_SESSION_JOIN) {\n const joined = handleMatchSessionJoinV0(socket, parsed, wss);\n const sessionId = String(parsed.sessionId || parsed.payload?.sessionId || \"\").trim();\n if (joined?.ok && sessionId) {\n sendMatchSessionSnapshotOnJoinV0(socket, sessionId);\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.MATCH_CASTLE_INVITE) {\n handleMatchCastleInviteV0(socket, parsed, wss);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.MATCH_MOVE) {\n handleMatchMoveAuthorityV0(socket, parsed, wss);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.MATCH_STATE_APPLIED) {\n handleMatchStateAppliedV0(socket, parsed, wss);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND) {\n const result = orchestrator.applyCommand(parsed.payload);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n }\n\n if (parsed.type === WS_MESSAGE.INPUT_FRAME) {\n const err = validateInputFrame(parsed.payload, socket);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const frame = parsed.payload.frame;\n const commands = Array.isArray(parsed.payload?.commands) ? parsed.payload.commands : [];\n orchestrator.queueInputFrame(socket.clientId, frame, commands);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, queued: true, frame })));\n }\n\n if (parsed.type === WS_MESSAGE.SIGNAL) {\n const payload = parsed.payload || {};\n if (Object.values(CASTLE_NETWORK_SIGNAL_V0).includes(payload.signalType)) {\n const netErr = validateCastleNetworkSignalPayloadV0(socket, payload);\n if (netErr) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: netErr })));\n return;\n }\n handleCastleNetworkSignalV0(socket, payload, wss);\n return;\n }\n const err = validateSignalPayload(payload);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const signalType = payload.signalType;\n const to = payload.to;\n for (const client of wss.clients) {\n if (client.clientId === to && client.readyState === 1) {\n client.send(JSON.stringify(createEnvelope(WS_MESSAGE.SIGNAL, { ...payload, from: socket.clientId })));\n break;\n }\n }\n }\n\n if (parsed.type === WS_MESSAGE.DRONE_TELEMETRY) {\n const payload = parsed.payload || {};\n const json = JSON.stringify(payload);\n if (Buffer.byteLength(json, \"utf8\") > 48 * 1024) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"DRONE_TELEMETRY payload too large.\" })));\n return;\n }\n broadcastExcept(createEnvelope(WS_MESSAGE.DRONE_TELEMETRY, { ...payload, relayFrom: socket.clientId }), socket);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_START) {\n handleRhizohVoiceLiveStartV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_CHUNK) {\n handleRhizohVoiceLiveChunkV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_STOP) {\n handleRhizohVoiceLiveStopV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_SOCIAL_PULSE) {\n handleCastleSocialPulse(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_WAL_PEER_FEED) {\n handleCastleWalPeerFeed(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.OPEN_DATA_QUERY) {\n queryOpenData(parsed.payload)\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: true, result })));\n })\n .catch((e) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: false, error: e.message })));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_QUERY) {\n runRhizohBrainV2({\n ...(parsed.payload || {}),\n uid: socket?.auth?.user?.uid || \"unknown\"\n })\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, result)));\n })\n .catch(async () => {\n const fallback = await runRhizohBrain(parsed.payload || {}).catch(() => ({\n ok: false,\n reply: \"Rhizoh temporary disturbance. Habitat remains stable.\"\n }));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, fallback)));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.BROADCAST_REGISTER) {\n const role = String(parsed.payload?.role || \"\");\n const serviceKind = String(parsed.payload?.kind || parsed.payload?.serviceKind || \"\").trim();\n const serviceId = String(parsed.payload?.serviceId || role || socket.clientId || \"\").trim();\n if (serviceKind && serviceId) {\n registerGatewayServiceNodeV0({\n kind: serviceKind,\n serviceId,\n gatewayClientId: socket.clientId,\n state: String(parsed.payload?.state || \"ONLINE\"),\n meta: parsed.payload?.meta\n });\n }\n if (role === \"GENESIS_BROADCAST_AGENT\") {\n broadcasterClientId = socket.clientId;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, registered: role })));\n broadcastState();\n } else if (serviceKind) {\n socket.send(\n JSON.stringify(\n createEnvelope(WS_MESSAGE.COMMAND_RESULT, {\n ok: true,\n registered: serviceKind,\n serviceId,\n interpretationOnly: true\n })\n )\n );\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.STUDIO_CUE) {\n // Only registered broadcaster can push global cues.\n if (!broadcasterClientId || socket.clientId !== broadcasterClientId) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Only broadcaster agent can emit STUDIO_CUE.\" })));\n return;\n }\n const cue = parsed.payload || {};\n if (typeof cue !== \"object\") {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid STUDIO_CUE payload.\" })));\n return;\n }\n broadcast(createEnvelope(WS_MESSAGE.STUDIO_CUE, cue));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_JOIN_ROOM) {\n const requested = String(parsed.payload?.roomId || \"\").trim();\n const requestedName = String(parsed.payload?.roomName || \"\").trim();\n const roomId = ensureRoom(requested || `room-${Math.random().toString(36).slice(2, 7)}`, requestedName || null);\n spiralState.activeByClient.set(socket.clientId, roomId);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, roomId })));\n broadcastSpiralState();\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_CREATE_CHARACTER) {\n const name = String(parsed.payload?.name || \"\").trim().slice(0, 32) || `AI-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;\n const role = String(parsed.payload?.role || \"agent\").trim().slice(0, 24) || \"agent\";\n const roomId = ensureRoom(\n String(parsed.payload?.roomId || spiralState.activeByClient.get(socket.clientId) || spiralState.rooms[0].id),\n null\n );\n\n // Character creation always maps to an authoritative world agent.\n const result = orchestrator.applyCommand({\n command: COMMAND.SPAWN_AGENT,\n params: { targetCastleId: \"castle-01\" }\n });\n if (!result?.ok || !result?.data?.id) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Character creation failed.\" })));\n return;\n }\n const id = result.data.id;\n spiralState.characters.set(id, { id, name, role, roomId, source: \"spawned\" });\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, characterId: id, roomId })));\n broadcastSpiralState();\n return;\n }\n });\n\n socket.on(\"close\", () => {\n if (socket.clientId === broadcasterClientId) broadcasterClientId = null;\n leaveMatchBroadcastRoomV0(socket);\n unregisterGatewayServiceNodesByClientV0(socket.clientId);\n clientStats.delete(socket.clientId);\n rhizohVoiceLiveSessions.delete(socket);\n spiralState.activeByClient.delete(socket.clientId);\n removeCastleSocialClientFromAllRooms(socket.clientId);\n removeCastleNetworkClientV0(socket.clientId, wss);\n broadcast(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }));\n broadcastState();\n });\n});\n\nsetInterval(() => {\n void (async () => {\n try {\n const { frame, snapshot, delta, packedDelta } = orchestrator.tick(1 / 20);\n syncSpiralCharactersWithWorld(snapshot);\n broadcast(createEnvelope(WS_MESSAGE.NETWORK_TICK, { frame }));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_TICK, snapshot));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA, delta));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA_PACKED, packedDelta));\n broadcastSpiralState();\n await runAcademyEventTick();\n const executed = await tickAutomations(Date.now());\n for (const a of executed) {\n await addTranscript(a.uid, {\n source: \"planner\",\n eventType: \"automation-executed\",\n text: `Automation executed: ${a.title} (${a.action})`,\n roomId: \"studio-main\",\n meta: { automationId: a.id, action: a.action, deviceId: a.deviceId || \"\" }\n });\n }\n } catch (e) {\n console.error(\"[GATEWAY] world tick / automations loop error:\", String(e?.message || e));\n }\n })();\n}, 50);\n\n(async () => {\n await rhizohRuntime.telemetry.initOpenTelemetry();\n if (String(process.env.CASTLE_GENESIS_DISK_PERSIST ?? \"\").trim() === \"\") {\n process.env.CASTLE_GENESIS_DISK_PERSIST = \"1\";\n }\n const genesisDiskBoot = await hydrateGenesisContinuityPersistenceBootV0();\n if (!genesisDiskBoot.ok) {\n console.warn(\"[GATEWAY] genesis disk hydrate failed; continuing with in-memory genesis continuity\");\n }\n httpServer.listen(PORT, () => {\n startGenesisCanonicalClock();\n installGenesisCheckpointSurfaceGetter(buildGenesisRuntimeSurfacePayloadLive);\n setGenesisContinuityAfterPublishHook((seq) => {\n noteGenesisCheckpointSeqCommitted(seq);\n noteGenesisContinuitySeqAcceptedV0(seq);\n });\n startGenesisContinuityInfraSampler(buildGenesisRuntimeSurfacePayloadLive, 2000);\n logProductionObservatorySurfaceGuardsV0();\n logGatewaySubstrateAuthorityGuardsV0();\n if (isLlmWorkerEnabledV0()) {\n try {\n const workerBoot = initLlmWorkerAtBootV0();\n console.log(\"[GATEWAY] llm worker boot:\", workerBoot);\n } catch (workerErr) {\n console.error(\"[GATEWAY] llm worker boot failed:\", workerErr);\n }\n }\n console.log(`[GATEWAY] ws/http://localhost:${PORT}`);\n console.log(\n `[GATEWAY] genesis continuity: GET ${rhizohRuntime.routes.genesisRuntime} | POST ${rhizohRuntime.routes.genesisIngress} | SSE ${rhizohRuntime.routes.genesisStream} | replay ${rhizohRuntime.routes.genesisReplay} | diff ${rhizohRuntime.routes.genesisReplayDiff} | equiv ${rhizohRuntime.routes.genesisReplayEquivalence} | analytics ${rhizohRuntime.routes.genesisReplayAnalytics} | evolution ${rhizohRuntime.routes.genesisReplayEvolution}`\n );\n if (String(process.env.CASTLE_GENESIS_OBSERVATORY_LIVE_V01 ?? \"\").trim() === \"1\") {\n console.log(\"GENESIS OBSERVATORY LIVE v0.1 — READ ONLY PHASE STARTED\");\n }\n });\n})().catch((e) => {\n console.error(\"[GATEWAY] fatal boot (before or during listen):\", e);\n process.exit(1);\n});\n"}, "f97a2c1f5085e9f5b8e9ad43c40785137c1088e4": {"t": 1785754118.552984, "n": 177633, "h": "dbcd3b418e33ffa2", "c": "import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport dotenv from \"dotenv\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconsole.log(\"🔥 [GENESIS_BOOT] entry file:\", __filename);\nconsole.log(\"🔥 [GENESIS_BOOT] node env:\", process.env.NODE_ENV);\nconsole.log(\"🔥 [GENESIS_BOOT] pid:\", process.pid);\nconsole.log(\"🧬 [GENESIS_BOOT] cwd:\", process.cwd());\nconsole.log(\"🧬 [GENESIS_BOOT] import.meta.url:\", import.meta.url);\ndotenv.config({ path: path.join(__dirname, \"..\", \".env\") });\ndotenv.config({ path: path.join(__dirname, \"..\", \".env.local\"), override: true });\n\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport { createServer } from \"node:http\";\nimport { createOrchestrator } from \"@castle/orchestrator\";\nimport { parseTextToCommand } from \"@castle/command-dsl\";\nimport {\n WS_MESSAGE,\n COMMAND,\n createEnvelope,\n createRhizohImmutableEventV0,\n createRhizohPayloadRefV0,\n safeJsonParse,\n validateRhizohVoiceLiveChunkPayloadV0\n} from \"@castle/protocol\";\nimport { queryOpenData } from \"./openData.js\";\nimport { verifyClientToken } from \"./auth.js\";\nimport { handleMatchMoveAuthorityV0, leaveMatchBroadcastRoomV0, sendMatchSessionSnapshotOnJoinV0 } from \"./rhizoh/matchMoveAuthorityV0.js\";\nimport { handleMatchSessionJoinV0, handleMatchCastleInviteV0 } from \"./rhizoh/matchBroadcastRoomV0.js\";\nimport { queryRhizohLlm } from \"./rhizohLlmGateway.js\";\nimport { rhizohGatewayTurn } from \"./rhizohGatewayTurn.js\";\nimport {\n enqueueLlmWorkerTaskAsyncV0,\n getLatestLlmWorkerTaskIdV0,\n getLlmWorkerDebugSnapshotV0,\n getLlmWorkerTaskSnapshotV0,\n initLlmWorkerAtBootV0,\n isLlmWorkerAsyncEnabledV0,\n isLlmWorkerEnabledV0,\n runLlmWorkerTaskV0\n} from \"./llmWorkerRuntime.js\";\nimport {\n RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n runRhizohVoiceTranscribeV3,\n rhizohVoiceTranscribeEnvV3\n} from \"./rhizohVoiceTranscribeV3.js\";\nimport { createHttpCorsPolicy, normalizeHttpOrigin, parseAllowedOriginsFromEnv } from \"./httpCorsPolicyV1.js\";\nimport {\n applyLanguagePropagationToLlmPayloadV1,\n applyLanguagePropagationToVoiceBodyV1,\n buildRhizohLanguagePropagationEchoV1,\n parseRhizohLanguagePropagationV1\n} from \"./rhizohLanguagePropagationV1.js\";\nimport { trimRhizohLlmGatewayPayloadV1 } from \"./rhizohLlmPayloadTrimV1.js\";\nimport {\n meshAppendDelta,\n meshContinuityAggregate,\n meshJoin,\n meshLeave,\n meshReplayFromNodeId,\n meshReplayFromSeq,\n meshSnapshot,\n meshSubscribeSse\n} from \"./presenceRoomMesh.js\";\nimport { runRhizohBrainV2 } from \"./rhizohBrainV2.js\";\nimport {\n listConnections,\n createConnection,\n updateConnection,\n deleteConnection,\n setDefaultConnection,\n resolveConnection\n} from \"./llmConnectionsStore.js\";\nimport { getCloudSyncSnapshotV0, mergeCloudSyncSnapshotV0 } from \"./castleCloudSyncStore.js\";\nimport {\n CASTLE_NETWORK_SIGNAL_V0,\n handleCastleNetworkSignalV0,\n listCastleNetworkPresenceV0,\n removeCastleNetworkClientV0,\n validateCastleNetworkSignalPayloadV0\n} from \"./castleNetworkRelayV0.js\";\nimport { checkHttpRateLimit, getHttpClientIp } from \"./castleHttpRateLimit.js\";\nimport {\n submitAbuseReportV0,\n listModerationQueueV0,\n softBlockUserV0,\n removeSoftBlockV0,\n listSoftBlockedUsersV0\n} from \"./ops/moderationMvpV0.js\";\nimport { listRecentAgentSnapshotsV0 } from \"./ops/agentObservabilityV0.js\";\nimport { readAgentContainmentConfigV0 } from \"./ops/agentContainmentV0.js\";\nimport { readCostContainmentConfigV0, getCostContainmentStatsV0 } from \"./ops/costContainmentV0.js\";\nimport { getGlobalCostLedgerSnapshotV0, getGclBackendStatusV0 } from \"./ops/globalCostLedgerV0.js\";\nimport { evaluateEconomicStrategyL3V0 } from \"./ops/economicStrategyEngineL3V0.js\";\nimport { getPhasedRolloutStatsV0, readPhasedRolloutTierV0 } from \"./ops/phasedRolloutV0.js\";\nimport { buildUnifiedStateNarrativeV0 } from \"./ops/unifiedStateNarrativeV0.js\";\nimport { resolveHardeningTenantIdV0 } from \"./ops/narrativeTenantIsolationV0.js\";\nimport {\n classifyStressResponseV0,\n verifyStressTaxonomyCoverageV0,\n verifyStressConflictResolutionV0\n} from \"./ops/stressResponseTaxonomyV0.js\";\nimport { verifyResolutionStabilityEnvelopeV0 } from \"./ops/resolutionStabilityEnvelopeV0.js\";\nimport { logLlmAccess } from \"./castleLlmAudit.js\";\nimport { appendMemory, listMemories, getMemoryContext, getPersonaGoalMemory, setPersonaGoalMemory, autoCompactMemories } from \"./memoryStore.js\";\nimport { getFirebasePersistence } from \"./firebasePersistence.js\";\nimport { registerAgentIdentity, listAgentIdentities, getAgentIdentity, updateAgentIdentity } from \"./agentIdentityStore.js\";\nimport { queueAcademyEvent, listAcademyEvents, resolveAcademyEvent, runAcademyEventTick } from \"./academyEventEngine.js\";\nimport { enqueuePdfJob, getPdfJob } from \"./pdfPipeline.js\";\nimport {\n createPublishSession,\n listPublishSessions,\n updatePublishSession,\n addTranscript,\n listTranscripts,\n searchMetadata,\n registerDevice,\n listDevices,\n updateDevice,\n appendTelemetry,\n listTelemetry,\n createAutomation,\n listAutomations,\n tickAutomations,\n enqueueCommand,\n listCommands,\n updateCommandStatus,\n registerSocialChannel,\n listSocialChannels\n} from \"./studioOpsStore.js\";\nimport { metrics as infraMetrics } from \"./infra/metrics.js\";\nimport { renderRhizohPrometheusMetrics, rhizohEnterpriseMetrics } from \"./infra/rhizohEnterpriseMetrics.js\";\nimport { scoreHealth } from \"./infra/healthScore.js\";\nimport { getTraceById, listTracesBySession } from \"./infra/traceRegistry.js\";\nimport { canonicalEpistemicSealString, hashAndSignEpistemicSeal, EPISTEMIC_SEAL_SCHEMA } from \"./epistemicSeal.js\";\nimport { persistEpistemicLedgerBatch } from \"./epistemicLedgerStore.js\";\nimport { persistEpistemicForecastBatch } from \"./epistemicForecastStore.js\";\nimport {\n persistAuthorityLedgerWitnessBatchV1,\n getAuthorityLedgerWitnessSnapshotV1,\n replayAuthorityLedgerWitnessChainV1\n} from \"./authorityLedgerWitnessStoreV1.js\";\nimport { assimilateAuthorityEpochMergeV1 } from \"./authorityEpochMergeStoreV1.js\";\nimport { buildRhizohExternalGroundTruthPayload } from \"./rhizohExternalGroundTruthGateway.js\";\nimport { ingestRhizohExternalLossBatchHttp } from \"./rhizohExternalLossIngestGateway.js\";\nimport {\n ingestRhizohProductOutcomeHttp,\n listRhizohProductOutcomeAggregates,\n verifyRhizohOutcomeHmac,\n verifyRhizohOutcomeSourceToken\n} from \"./rhizohProductOutcomeIngestGateway.js\";\nimport { initRhizoh } from \"./rhizohProductionBootstrap.js\";\nimport { handleAcademicObservatoryExportGetV0 } from \"./rhizoh/academicObservatoryHttpV0.js\";\nimport { handleCouncilAnomalyReasoningPostV0 } from \"./council/rhizohEpistemicCouncilHttpV0.js\";\nimport {\n handleLiveSportsBundleGetV0,\n handleLiveNewsHeadlinesGetV0,\n handleLiveWorldFeedGetV0\n} from \"./rhizoh/liveWorldFeedHttpV0.js\";\nimport {\n buildGenesisRuntimeSurfacePayload,\n recordGenesisEpistemicLedgerPersisted,\n recordGenesisEpistemicSealIssued,\n startGenesisCanonicalClock\n} from \"./genesisRuntimeSurfaceV0.js\";\nimport { registerGenesisSseClient, getGenesisContinuitySeq, setGenesisContinuityAfterPublishHook } from \"./genesisContinuityStreamHubV0.js\";\nimport { computeGenesisReplayFingerprintV0 } from \"./genesisReplayFingerprintV0.js\";\nimport { buildGenesisGatewayCapabilitiesV0 } from \"./genesisGatewayCapabilitiesV0.js\";\nimport {\n installGenesisCheckpointSurfaceGetter,\n noteGenesisCheckpointSeqCommitted,\n getLatestGenesisSignedCheckpoint\n} from \"./genesisContinuityCheckpointV0.js\";\nimport { hydrateGenesisContinuityPersistenceBootV0 } from \"./genesisContinuityHydrateBootV0.js\";\nimport { startGenesisContinuityInfraSampler } from \"./genesisContinuityInfraSamplerV0.js\";\nimport {\n genesisCheckpointQueryBySeqV0,\n genesisCheckpointQueryRangeV0,\n genesisCheckpointQueryLineageV0\n} from \"./genesisCheckpointQueryV0.js\";\nimport {\n genesisContinuityEventArchiveEnabled,\n queryGenesisContinuityEventArchiveV0,\n GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION\n} from \"./genesisContinuityEventArchiveV0.js\";\nimport { resolveGenesisReplayRouterV1, GENESIS_REPLAY_ROUTER_SCHEMA } from \"./genesisReplayRouterV1.js\";\nimport {\n computeGenesisReplayTemporalDiffV1,\n GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA\n} from \"./genesisReplayTemporalDiffV1.js\";\nimport { compareGenesisReplayEquivalenceV1 } from \"./genesisReplayEquivalenceV1.js\";\nimport { computeGenesisReplayAnalyticsV1 } from \"./genesisReplayAnalyticsV1.js\";\nimport { computeGenesisReplayEvolutionV1 } from \"./genesisReplayEvolutionV1.js\";\nimport { handleStripeCheckoutCreate, handleStripeMembershipWebhook } from \"./stripeWebhookMembership.js\";\nimport { buildRealityHealthPayload } from \"./edgeRealityConsistencyV1.js\";\nimport {\n assertWalPeerSocketAuthorityV0,\n buildGatewaySubstrateAuthoritySnapshotV0,\n logGatewaySubstrateAuthorityGuardsV0,\n validateWalPeerFeedSignatureV0\n} from \"./gatewaySubstrateAuthorityV0.js\";\nimport {\n buildSubstrateOperationalSnapshotV0,\n recordWalPeerRejectV0,\n renderSubstratePrometheusMetricsV0\n} from \"./infra/substrateOperationalMetrics.js\";\nimport { ingestClientSubstrateHealthV0 } from \"./rhizohSubstrateHealthIngestV0.js\";\nimport { recordSubstrateOtelEventV0 } from \"./infra/opentelemetrySubstrateV0.js\";\nimport {\n GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n getGenesisWorldObservationIngressStatusV0,\n ingestGenesisWorldObservationV0\n} from \"./genesisContinuityClientIngressV0.js\";\nimport {\n getGenesisContinuitySeqAuditV0,\n noteGenesisContinuitySeqAcceptedV0\n} from \"./genesisContinuitySeqGapV0.js\";\n\n/** Render / Fly / Railway: `PORT` — yoksa `CASTLE_GATEWAY_PORT` — yoksa 8090. */\nconst PORT =\n Number(process.env.PORT) ||\n Number(process.env.CASTLE_GATEWAY_PORT) ||\n 8090;\n/** Spiral / world WebSocket server — set after `wss` init; used by genesis runtime surface. */\nlet spiralWssForGenesis = null;\nconst MAX_MESSAGE_BYTES = Number(process.env.CASTLE_MAX_MESSAGE_BYTES || 32 * 1024);\nconst REQUIRED_GATEWAY_TOKEN = process.env.CASTLE_GATEWAY_TOKEN || \"\";\nconst ALLOWED_ORIGINS = parseAllowedOriginsFromEnv().fromList;\nconst httpCors = createHttpCorsPolicy();\n\n/** @returns {string|null} `Access-Control-Allow-Origin`; null = browser CORS fail. */\nfunction accessControlAllowOriginValue(req) {\n return httpCors.accessControlAllowOriginValue(req);\n}\n\nfunction applyHttpCorsHeaders(req, res) {\n return httpCors.applyHttpCorsHeaders(req, res);\n}\nconst REQUIRE_AUTH = process.env.CASTLE_REQUIRE_AUTH === \"true\";\nconst ALLOW_DEV_ANON = process.env.CASTLE_ALLOW_DEV_ANON !== \"false\";\nconst ALLOW_DEV_HTTP_UID = process.env.CASTLE_ALLOW_DEV_HTTP_UID !== \"false\";\n/** Presence mesh: allow `X-Castle-Guest-Id` when Firebase auth absent (default on). Set `false` to require auth only. */\nconst ALLOW_MESH_GUEST = process.env.CASTLE_ALLOW_MESH_GUEST !== \"false\";\nconst SOCIAL_RETRY_MAX = Math.max(0, Math.min(5, Number(process.env.CASTLE_SOCIAL_RETRY_MAX || 2)));\nconst SOCIAL_RETRY_BASE_MS = Math.max(100, Number(process.env.CASTLE_SOCIAL_RETRY_BASE_MS || 700));\nconst TELEGRAM_BOT_TOKEN = process.env.CASTLE_TELEGRAM_BOT_TOKEN || \"\";\nconst WHATSAPP_TOKEN = process.env.CASTLE_WHATSAPP_TOKEN || \"\";\nconst WHATSAPP_PHONE_NUMBER_ID = process.env.CASTLE_WHATSAPP_PHONE_NUMBER_ID || \"\";\nconst RL_RHIZOH_LLM_PER_MIN = Math.max(5, Number(process.env.CASTLE_RL_RHIZOH_LLM_PER_MIN || 40));\nconst RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN || 30)\n);\nconst RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN || 120));\nconst RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN = Math.max(\n 10,\n Number(process.env.CASTLE_RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN || 60)\n);\nconst RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN = Math.max(8, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN || 48));\nconst RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN = Math.max(4, Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN || 24));\nconst RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN || 18)\n);\nconst RL_EPISTEMIC_SEAL_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_EPISTEMIC_SEAL_PER_MIN || 120));\nconst RL_EPISTEMIC_LOG_PER_MIN = Math.max(20, Number(process.env.CASTLE_RL_EPISTEMIC_LOG_PER_MIN || 240));\nconst EPISTEMIC_SEAL_SECRET = String(\n process.env.CASTLE_EPISTEMIC_SEAL_SECRET || process.env.CASTLE_GATEWAY_TOKEN || \"\"\n).trim();\nconst RL_LLM_CONN_TEST_PER_MIN = Math.max(2, Number(process.env.CASTLE_RL_LLM_CONNECTION_TEST_PER_MIN || 8));\n\n/** One-shot production warnings: CORS surface, auth toggle, dev bypass flags (observatory deploy hygiene). */\nfunction logProductionObservatorySurfaceGuardsV0() {\n if (String(process.env.NODE_ENV || \"\").toLowerCase() !== \"production\") return;\n const tag = \"[GATEWAY_PROD_GUARD]\";\n const primaryCors = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n const corsListEmpty = ALLOWED_ORIGINS.length === 0;\n if (corsListEmpty && !primaryCors) {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty and CASTLE_HTTP_CORS_ORIGIN empty — production still merges RHIZOH_BROWSER_ORIGINS_V1 defaults; set env for preview/staging hosts`\n );\n } else if (corsListEmpty && primaryCors && primaryCors !== \"*\") {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty — only CASTLE_HTTP_CORS_ORIGIN set; add web.app + firebaseapp.com (and previews) to CASTLE_ALLOWED_ORIGINS if clients use alternate hostnames`\n );\n }\n if (primaryCors && /localhost|127\\.0\\.0\\.1/i.test(primaryCors)) {\n console.warn(`${tag} CORS: CASTLE_HTTP_CORS_ORIGIN is localhost while NODE_ENV=production`);\n }\n if (REQUIRE_AUTH) {\n console.warn(\n `${tag} AUTH: CASTLE_REQUIRE_AUTH=true — clients must send a valid gateway credential (otherwise silent 401 on replay/SSE); EventSource cannot set custom headers unless proxied`\n );\n if (REQUIRED_GATEWAY_TOKEN.length < 16) {\n console.warn(`${tag} AUTH: CASTLE_GATEWAY_TOKEN missing or short while require-auth enabled`);\n }\n }\n if (ALLOW_DEV_ANON) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_ANON is not \"false\" — dev anonymous path may be live in production (set CASTLE_ALLOW_DEV_ANON=false)`\n );\n }\n if (ALLOW_DEV_HTTP_UID) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_HTTP_UID is not \"false\" — X-Castle-Dev-Uid bypass risk (set CASTLE_ALLOW_DEV_HTTP_UID=false)`\n );\n }\n}\n\nconst rhizohRuntime = initRhizoh();\n\n/** İstemci: llmKeySource veya keySource — env | user_connection | auto */\nfunction normalizeClientLlmKeySource(raw) {\n const s = String(raw ?? \"\").trim().toLowerCase();\n if (s === \"env\" || s === \"server\") return \"env\";\n if (s === \"user_connection\" || s === \"connection\" || s === \"user\") return \"user_connection\";\n if (s === \"auto\" || s === \"\") return \"auto\";\n return null;\n}\n\n/**\n * Server-side provider wins for env/auto unless user_connection — client bundle may still send provider=openai.\n * @param {\"env\"|\"user_connection\"|\"auto\"} keyMode\n * @param {{ provider?: string, model?: string, apiKey?: string } | null} conn\n * @param {{ provider?: string, model?: string }} payload\n */\nfunction resolveServerLlmProviderV0(keyMode, conn, payload) {\n const envProvider = String(process.env.CASTLE_LLM_PROVIDER || \"\").trim() || undefined;\n const envModel = String(process.env.CASTLE_LLM_MODEL || \"\").trim() || undefined;\n\n if (keyMode === \"user_connection\") {\n return {\n resolvedProvider: conn?.provider || payload?.provider,\n resolvedModel: conn?.model || payload?.model,\n connApiKey: conn?.apiKey || \"\"\n };\n }\n\n if (keyMode === \"env\") {\n return {\n resolvedProvider: envProvider ?? payload?.provider,\n resolvedModel: envModel ?? payload?.model,\n connApiKey: \"\"\n };\n }\n\n if (conn?.provider && conn?.apiKey) {\n return {\n resolvedProvider: conn.provider,\n resolvedModel: conn.model,\n connApiKey: conn.apiKey\n };\n }\n\n return {\n resolvedProvider: envProvider ?? payload?.provider,\n resolvedModel: envModel ?? payload?.model,\n connApiKey: \"\"\n };\n}\n\nfunction jsonReplacerSafeV0(_key, value) {\n if (typeof value === \"bigint\") return Number(value);\n return value;\n}\n\nfunction sendJson(res, statusCode, payload) {\n res.writeHead(statusCode, { \"Content-Type\": \"application/json; charset=utf-8\" });\n try {\n res.end(JSON.stringify(payload, jsonReplacerSafeV0));\n } catch (error) {\n res.end(\n JSON.stringify({\n ok: false,\n error: \"response_serialize_failed\",\n detail: String(error?.message || error)\n })\n );\n }\n}\n\nfunction logRhizohHealth(stage, detail = {}) {\n try {\n console.info(`[CASTLE_${String(stage || \"unknown\")}]`, detail && typeof detail === \"object\" ? detail : {});\n } catch {\n /* noop */\n }\n}\n\nfunction renderInfraMetricsProm() {\n const lines = [\n `castle_gateway_events_processed_total ${infraMetrics.eventsProcessed}`,\n `castle_gateway_duplicate_reject_total ${infraMetrics.duplicateRejects}`,\n `castle_gateway_enqueue_latency_ms ${infraMetrics.enqueueLatencyMs}`,\n `castle_gateway_queue_depth ${infraMetrics.queueDepth}`,\n `castle_gateway_queue_lag_seconds ${(Number(infraMetrics.queueLag || 0) / 1000).toFixed(3)}`,\n `castle_gateway_errors_total ${infraMetrics.errors}`\n ];\n const buckets = infraMetrics.enqueueLatencyBuckets || {};\n for (const k of Object.keys(buckets)) {\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"${k}\"} ${buckets[k]}`);\n }\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"+Inf\"} ${infraMetrics.eventsProcessed}`);\n if (process.env.CASTLE_SUBSTRATE_METRICS !== \"0\") {\n lines.push(renderSubstratePrometheusMetricsV0().trimEnd());\n }\n return lines.join(\"\\n\");\n}\n\nasync function fetchJsonWithTimeout(url, timeoutMs = 1200) {\n const ctl = new AbortController();\n const tid = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const res = await fetch(url, { signal: ctl.signal });\n if (!res.ok) return null;\n return await res.json();\n } catch {\n return null;\n } finally {\n clearTimeout(tid);\n }\n}\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction getHttpPathname(req) {\n let p = String(req.url || \"\").split(\"?\")[0];\n /* Reverse proxies / clients sometimes append `/` — avoid silent 404 on e.g. `/rhizoh/genesis/stream/`. */\n while (p.length > 1 && p.endsWith(\"/\")) {\n p = p.slice(0, -1);\n }\n return p;\n}\n\nfunction rhizohLlmEnvConfigured() {\n const p = String(process.env.CASTLE_LLM_PROVIDER || \"openai\").toLowerCase();\n const key =\n p === \"anthropic\"\n ? process.env.ANTHROPIC_API_KEY\n : p === \"gemini\"\n ? process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY\n : p === \"xai\"\n ? process.env.XAI_API_KEY\n : p === \"deepseek\"\n ? process.env.DEEPSEEK_API_KEY\n : p === \"mistral\"\n ? process.env.MISTRAL_API_KEY\n : p === \"openrouter\"\n ? process.env.OPENROUTER_API_KEY\n : process.env.OPENAI_API_KEY;\n return !!String(key || \"\").trim();\n}\n\n/**\n * @param {import(\"firebase-admin/firestore\").Firestore | null} db\n * @param {number} timeoutMs\n */\nasync function probeFirestoreReachable(db, timeoutMs = 2000) {\n if (!db) return { ok: false, reason: \"no_db\" };\n const timeout = sleep(timeoutMs).then(() => ({ ok: false, reason: \"timeout\" }));\n const work = db\n .collection(\"_castle_gateway_health\")\n .doc(\"ping\")\n .get()\n .then(() => ({ ok: true, reason: \"reachable\" }))\n .catch((e) => ({ ok: false, reason: String(e?.message || e || \"firestore_error\") }));\n return Promise.race([work, timeout]);\n}\n\nfunction stripHtml(text = \"\") {\n return String(text)\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \" \")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \" \")\n .replace(/<[^>]+>/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction haversineMeters(aLat, aLon, bLat, bLon) {\n const R = 6371000;\n const dLat = ((bLat - aLat) * Math.PI) / 180;\n const dLon = ((bLon - aLon) * Math.PI) / 180;\n const sa = Math.sin(dLat / 2);\n const sb = Math.sin(dLon / 2);\n const x = sa * sa + Math.cos((aLat * Math.PI) / 180) * Math.cos((bLat * Math.PI) / 180) * sb * sb;\n return 2 * R * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));\n}\n\nasync function checkPublishTargetHealth(protocol, target) {\n const p = String(protocol || \"\").toUpperCase();\n const t = String(target || \"\").trim();\n if (!t) return { ok: false, reason: \"target_missing\", shouldReconnect: false };\n if (p === \"RTMP\") {\n const ok = t.startsWith(\"rtmp://\") || t.startsWith(\"rtmps://\");\n return { ok, reason: ok ? \"rtmp_target_format_ok\" : \"invalid_rtmp_target\", shouldReconnect: !ok, reconnectInSec: 15 };\n }\n if (p === \"WHIP\" || p === \"SFU\") {\n try {\n const res = await fetch(t, { method: \"OPTIONS\" });\n const ok = res.status < 500;\n return { ok, reason: `http_${res.status}`, shouldReconnect: !ok, reconnectInSec: 8 };\n } catch {\n return { ok: false, reason: \"network_unreachable\", shouldReconnect: true, reconnectInSec: 8 };\n }\n }\n return { ok: true, reason: \"unchecked_protocol\", shouldReconnect: false };\n}\n\nasync function fetchTextFromUrl(url) {\n const res = await fetch(url, { method: \"GET\" });\n if (!res.ok) throw new Error(`fetch_${res.status}`);\n const ct = String(res.headers.get(\"content-type\") || \"\");\n if (ct.includes(\"application/pdf\")) throw new Error(\"pdf_binary_not_supported_directly\");\n const body = await res.text();\n return stripHtml(body).slice(0, 14000);\n}\n\nfunction buildTaskActionGraph(task = \"\", context = {}) {\n const t = String(task || \"\").toLowerCase();\n if (t.includes(\"dinner\") || t.includes(\"akşam yemeği\") || t.includes(\"aksam yemegi\")) {\n return {\n taskTitle: \"Dinner Prep Planner\",\n nodes: [\n { id: \"n1\", title: \"Kitchen preheat\", action: \"smart-oven.preheat\", params: { temperatureC: 190, mode: \"fan\" } },\n { id: \"n2\", title: \"Lighting comfort\", action: \"smart-light.set_scene\", params: { scene: \"warm_dinner\" } },\n { id: \"n3\", title: \"Fridge inventory check\", action: \"smart-fridge.inventory_scan\", params: {} },\n { id: \"n4\", title: \"Reminder notification\", action: \"smart-watch.notify\", params: { text: \"Dinner prep started.\" } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" },\n { from: \"n3\", to: \"n4\" }\n ]\n };\n }\n return {\n taskTitle: task || \"General Planner\",\n nodes: [\n { id: \"n1\", title: \"Safety check\", action: \"device.safety_check\", params: { context } },\n { id: \"n2\", title: \"Primary action\", action: \"device.execute_task\", params: { task } },\n { id: \"n3\", title: \"Report\", action: \"studio.timeline_note\", params: { task } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" }\n ]\n };\n}\n\nfunction buildEthicsPrograms() {\n return {\n policyVersion: \"v1\",\n tracks: [\n {\n id: \"elderly-care-assist\",\n audience: \"elderly\",\n title: \"Elderly Assistance Track\",\n principles: [\"human-in-the-loop\", \"consent-first\", \"low-friction alerts\"],\n modules: [\"medication-reminder\", \"fall-risk-check\", \"family-notify\", \"daily-companion-brief\"]\n },\n {\n id: \"kids-safe-learning\",\n audience: \"children\",\n title: \"Kids Safe Learning Track\",\n principles: [\"guardian-consent\", \"age-appropriate-language\", \"content-filtering\"],\n modules: [\"study-coach\", \"screen-time-balance\", \"safe-social-share\", \"emergency-contact\"]\n }\n ]\n };\n}\n\nasync function deliverViaTelegram(channel, text) {\n const chatId = String(channel.endpoint || \"\").trim();\n if (!TELEGRAM_BOT_TOKEN) throw new Error(\"telegram_token_missing\");\n if (!chatId) throw new Error(\"telegram_chat_id_missing\");\n const url = `https://api.telegram.org/bot${encodeURIComponent(TELEGRAM_BOT_TOKEN)}/sendMessage`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ chat_id: chatId, text })\n });\n if (!res.ok) throw new Error(`telegram_http_${res.status}`);\n return { provider: \"telegram\", status: res.status };\n}\n\nasync function deliverViaWhatsapp(channel, text) {\n const to = String(channel.endpoint || \"\").trim();\n if (!WHATSAPP_TOKEN) throw new Error(\"whatsapp_token_missing\");\n if (!WHATSAPP_PHONE_NUMBER_ID) throw new Error(\"whatsapp_phone_number_id_missing\");\n if (!to) throw new Error(\"whatsapp_to_missing\");\n const url = `https://graph.facebook.com/v22.0/${encodeURIComponent(WHATSAPP_PHONE_NUMBER_ID)}/messages`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${WHATSAPP_TOKEN}` },\n body: JSON.stringify({ messaging_product: \"whatsapp\", to, type: \"text\", text: { body: text } })\n });\n if (!res.ok) throw new Error(`whatsapp_http_${res.status}`);\n return { provider: \"whatsapp\", status: res.status };\n}\n\nasync function deliverViaWebhook(channel, text) {\n const endpoint = String(channel.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"webhook_endpoint_missing\");\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text, platform: channel.platform || \"webhook\", ts: Date.now() })\n });\n if (!res.ok) throw new Error(`webhook_http_${res.status}`);\n return { provider: \"webhook\", status: res.status };\n}\n\nasync function deliverSocialMessage(channel, text) {\n const platform = String(channel.platform || \"webhook\").toLowerCase();\n if (platform === \"telegram\") return deliverViaTelegram(channel, text);\n if (platform === \"whatsapp\") return deliverViaWhatsapp(channel, text);\n return deliverViaWebhook(channel, text);\n}\n\nasync function deliverSocialWithRetry(channel, text) {\n let attempt = 0;\n let lastError = null;\n while (attempt <= SOCIAL_RETRY_MAX) {\n try {\n const result = await deliverSocialMessage(channel, text);\n return { ok: true, attempt: attempt + 1, ...result };\n } catch (error) {\n lastError = error;\n if (attempt >= SOCIAL_RETRY_MAX) break;\n const jitter = Math.floor(Math.random() * 90);\n await sleep(SOCIAL_RETRY_BASE_MS * Math.pow(2, attempt) + jitter);\n }\n attempt += 1;\n }\n return { ok: false, attempt: attempt + 1, error: String(lastError?.message || \"delivery_failed\") };\n}\n\nasync function dispatchCommandToAdapter(device, command) {\n const adapter = String(command?.adapter || device?.adapter || \"websocket\").toLowerCase();\n const endpoint = String(device?.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"device_endpoint_missing\");\n if (adapter === \"mqtt\") {\n // MQTT delivery is proxied via a bridge endpoint (HTTP webhook style).\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n type: \"mqtt-command\",\n topic: `castle/devices/${device.id}/commands`,\n action: command.action,\n params: command.params || {}\n })\n });\n if (!res.ok) throw new Error(`mqtt_bridge_http_${res.status}`);\n return { ok: true, adapter, via: \"http-bridge\", status: res.status };\n }\n if (!endpoint.startsWith(\"ws://\") && !endpoint.startsWith(\"wss://\")) throw new Error(\"invalid_websocket_endpoint\");\n const sendResult = await new Promise((resolve, reject) => {\n const ws = new WebSocket(endpoint);\n const timer = setTimeout(() => {\n try {\n ws.close();\n } catch {\n /* noop */\n }\n reject(new Error(\"websocket_timeout\"));\n }, 3000);\n ws.on(\"open\", () => {\n ws.send(\n JSON.stringify({\n type: \"castle-command\",\n commandId: command.id,\n deviceId: command.deviceId,\n action: command.action,\n params: command.params || {}\n }),\n (err) => {\n clearTimeout(timer);\n ws.close();\n if (err) reject(new Error(\"websocket_send_failed\"));\n else resolve({ ok: true, adapter: \"websocket\", via: \"ws\" });\n }\n );\n });\n ws.on(\"error\", () => {\n clearTimeout(timer);\n reject(new Error(\"websocket_connect_failed\"));\n });\n });\n return sendResult;\n}\n\nfunction readHttpJson(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => {\n if (!buf) return resolve({});\n const payload = safeJsonParse(buf);\n if (!payload || typeof payload !== \"object\") return reject(new Error(\"invalid_json_payload\"));\n resolve(payload);\n });\n req.on(\"error\", reject);\n });\n}\n\n/** Ham gövde (HMAC doğrulaması için). */\nfunction readHttpBodyText(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => resolve(buf));\n req.on(\"error\", reject);\n });\n}\n\nfunction maskOpaqueId(value) {\n const t = String(value || \"\");\n if (!t) return \"(empty)\";\n if (t.length <= 8) return \"****\";\n return `${t.slice(0, 4)}…${t.slice(-2)}`;\n}\n\nfunction readGatewayHttpToken(req) {\n const x = String(req.headers?.[\"x-castle-gateway-token\"] || \"\").trim();\n if (x) return x;\n const auth = String(req.headers?.authorization || \"\");\n const m = /^Bearer\\s+(.+)$/i.exec(auth);\n return m ? String(m[1] || \"\").trim() : \"\";\n}\n\nasync function resolveHttpUser(req) {\n const authResult = await verifyClientToken(req);\n if (authResult?.ok && authResult.user?.uid) {\n return { ok: true, uid: String(authResult.user.uid), mode: authResult.kind || \"auth\" };\n }\n if (ALLOW_DEV_ANON && ALLOW_DEV_HTTP_UID) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) return { ok: true, uid: `dev-${devUid.slice(0, 64)}`, mode: \"dev-header\" };\n }\n return { ok: false, reason: authResult?.reason || \"auth_required\" };\n}\n\n/**\n * Epistemic ledger ingest — Firebase uid OR valid transport token + X-Castle-Dev-Uid.\n * Prod disables CASTLE_ALLOW_DEV_HTTP_UID; LLM may still work while telemetry 401'd without this.\n */\nasync function resolveEpistemicIngestActor(req) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok && tok === REQUIRED_GATEWAY_TOKEN) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) {\n return { ok: true, uid: `gw-telemetry-${devUid.slice(0, 64)}`, mode: \"gateway-token-dev-uid\" };\n }\n return { ok: false, reason: \"dev_uid_required\" };\n }\n if (tok) {\n return { ok: false, reason: \"gateway_token_mismatch\" };\n }\n }\n const auth = await resolveHttpUser(req);\n if (auth.ok) return auth;\n return { ok: false, reason: auth.reason || \"auth_required\" };\n}\n\n/** Mesh actor: Firebase uid, dev header, or `guest:<uuid>` via header / `guestId` query (SSE cannot set headers). */\nasync function resolveMeshActor(req) {\n const auth = await resolveHttpUser(req);\n if (auth.ok) return { ok: true, clientUid: auth.uid, mode: auth.mode };\n if (ALLOW_MESH_GUEST) {\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const gid = String(req.headers?.[\"x-castle-guest-id\"] || q.searchParams.get(\"guestId\") || \"\").trim();\n if (gid) {\n const normalized = gid.startsWith(\"guest:\") ? gid.slice(0, 180) : `guest:${gid.slice(0, 160)}`;\n return { ok: true, clientUid: normalized, mode: \"guest-header\" };\n }\n }\n return { ok: false, reason: \"mesh_auth_required\" };\n}\n\n/** Live genesis runtime JSON (GET /rhizoh/genesis/runtime + SSE infra sampler). */\nasync function buildGenesisRuntimeSurfacePayloadLive() {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n let spiralWsActive = 0;\n if (spiralWssForGenesis) {\n for (const c of spiralWssForGenesis.clients) {\n if (c.readyState === 1) spiralWsActive += 1;\n }\n }\n const base = buildGenesisRuntimeSurfacePayload({\n infraMetrics,\n scoredHealth: scored,\n rhizohEnterpriseMetrics,\n mesh: meshContinuityAggregate(),\n workerHealth: worker,\n port: PORT,\n spiralWebSocketClientsActive: spiralWsActive\n });\n return {\n ...base,\n replayFingerprint: computeGenesisReplayFingerprintV0(base),\n gatewayCapabilities: buildGenesisGatewayCapabilitiesV0(),\n genesisStream: {\n schema: \"castle.genesis.stream_cursor.v0\",\n lastAcceptedSeq: getGenesisContinuitySeq(),\n clientIngress: {\n schema: GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n contract: \"castle.world_observation.ingress_envelope.v1\",\n postPath: rhizohRuntime.routes.genesisIngress,\n closure: getGenesisWorldObservationIngressStatusV0()\n },\n seqContinuity: getGenesisContinuitySeqAuditV0(),\n eventArchive: genesisContinuityEventArchiveEnabled()\n ? {\n enabled: true,\n projection: GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION,\n queryPath: rhizohRuntime.routes.genesisContinuityEvents\n }\n : { enabled: false },\n replayRouter: {\n schema: GENESIS_REPLAY_ROUTER_SCHEMA,\n queryPath: rhizohRuntime.routes.genesisReplay,\n queryHint: \"from=&to= or range=from-to; optional type=&checkpoints=0\",\n temporalDiffPath: rhizohRuntime.routes.genesisReplayDiff,\n temporalDiffSchema: GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA,\n equivalencePath: rhizohRuntime.routes.genesisReplayEquivalence,\n analyticsPath: rhizohRuntime.routes.genesisReplayAnalytics,\n evolutionPath: rhizohRuntime.routes.genesisReplayEvolution\n }\n }\n };\n}\n\nconst httpServer = createServer(async (req, res) => {\n const corsAllow = applyHttpCorsHeaders(req, res);\n if (req.method === \"OPTIONS\") {\n if (!corsAllow && req.headers?.origin) {\n console.warn(\"[GATEWAY_CORS] preflight rejected for origin:\", req.headers.origin);\n }\n res.writeHead(corsAllow ? 204 : 403);\n res.end();\n return;\n }\n\n const pathname = getHttpPathname(req);\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/genesis/__ping\") {\n sendJson(res, 200, {\n ok: true,\n pid: process.pid,\n entry: __filename,\n time: Date.now(),\n genesis: true\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/epistemic/__probe\") {\n sendJson(res, 200, {\n ok: true,\n epistemicIngestActorV2: true,\n routes: {\n seal: rhizohRuntime.routes.epistemicSeal,\n logsBatch: rhizohRuntime.routes.epistemicLogsBatch,\n authorityLedgerBatch: rhizohRuntime.routes.authorityLedgerBatch,\n authorityLedgerReplay: rhizohRuntime.routes.authorityLedgerReplay,\n authorityEpochMerge: rhizohRuntime.routes.authorityEpochMerge\n },\n hasGatewayToken: Boolean(REQUIRED_GATEWAY_TOKEN),\n gatewayTokenLen: REQUIRED_GATEWAY_TOKEN ? REQUIRED_GATEWAY_TOKEN.length : 0\n });\n return;\n }\n\n if (\n req.method === \"GET\" &&\n (pathname === \"/rhizoh/live/world-feed\" ||\n pathname === \"/rhizoh/live/sports-bundle\" ||\n pathname === \"/rhizoh/live/news-headlines\")\n ) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_live_world_feed:${ip}`, RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const out =\n pathname === \"/rhizoh/live/sports-bundle\"\n ? await handleLiveSportsBundleGetV0()\n : pathname === \"/rhizoh/live/news-headlines\"\n ? await handleLiveNewsHeadlinesGetV0(req)\n : await handleLiveWorldFeedGetV0(req);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/webhooks/stripe\") {\n await handleStripeMembershipWebhook(req, res);\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/billing/stripe/checkout\") {\n await handleStripeCheckoutCreate(req, res, { readHttpJson, resolveHttpUser, sendJson });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/live\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, live: false, dns: true, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n sendJson(res, 200, { ok: true, live: true, dns: true, service: \"castle-gateway\", ts: Date.now() });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/cors\") {\n const reqOrigin = normalizeHttpOrigin(req.headers?.origin);\n const allowOrigin = accessControlAllowOriginValue(req);\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n requestOrigin: reqOrigin || null,\n allowed: Boolean(allowOrigin),\n allowOrigin: allowOrigin || null,\n allowSetSize: httpCors.allowSet.size\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/ready\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, ready: false, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n try {\n const persistence = getFirebasePersistence();\n const llmProbe = rhizohLlmEnvConfigured();\n sendJson(res, 200, {\n ok: true,\n ready: true,\n dns: true,\n persistence: persistence.mode,\n service: \"castle-gateway\",\n gateway: \"ok\",\n llm: llmProbe ? \"ok\" : \"degraded\",\n spine: llmProbe ? \"ok\" : \"partial\"\n });\n } catch (e) {\n sendJson(res, 503, { ok: false, ready: false, reason: String(e?.message || e), service: \"castle-gateway\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/reality\") {\n const payload = buildRealityHealthPayload();\n sendJson(res, payload.ok ? 200 : 503, payload);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/substrate\") {\n const authority = buildGatewaySubstrateAuthoritySnapshotV0();\n const operational = buildSubstrateOperationalSnapshotV0();\n const drift = buildRealityHealthPayload();\n const ok = Boolean(authority.productionAuthoritySatisfied) && drift.ok;\n sendJson(res, ok ? 200 : 503, {\n ok,\n service: \"castle-gateway\",\n ts: Date.now(),\n authority,\n operational,\n drift: { ok: drift.ok, errors: drift.drift?.errors, warnings: drift.drift?.warnings }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.councilAnomalyReasoning) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_council_anomaly:${ip}`, 30, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const body = await readHttpJson(req);\n const out = handleCouncilAnomalyReasoningPostV0(body);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/substrate/health\") {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const body = await readHttpJson(req);\n const uid = (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestClientSubstrateHealthV0(body, { uid, clientId: uid });\n if (!result.ok) {\n sendJson(res, 400, result);\n return;\n }\n recordSubstrateOtelEventV0(\"client_health_ingest\", { uid: String(uid).slice(0, 32) });\n sendJson(res, 200, result);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/deps\") {\n const t0 = Date.now();\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, {\n ok: false,\n dns: true,\n llm: false,\n firestore: false,\n phase: \"maintenance\",\n latencyMs: Date.now() - t0,\n service: \"castle-gateway\"\n });\n return;\n }\n const persistence = getFirebasePersistence();\n let firestoreOk = persistence.mode !== \"firebase\";\n let firestoreDetail = persistence.mode === \"file\" ? \"file_mode\" : \"not_applicable\";\n if (persistence.mode === \"firebase\" && persistence.db) {\n const pr = await probeFirestoreReachable(persistence.db, 2000);\n firestoreOk = pr.ok;\n firestoreDetail = pr.reason || (pr.ok ? \"reachable\" : \"fail\");\n } else if (persistence.mode === \"firebase\" && !persistence.db) {\n firestoreOk = false;\n firestoreDetail = \"firebase_not_initialized\";\n }\n const llmOk = rhizohLlmEnvConfigured();\n const overallOk = llmOk && firestoreOk;\n const latencyMs = Date.now() - t0;\n sendJson(res, 200, {\n ok: overallOk,\n dns: true,\n llm: llmOk,\n firestore: persistence.mode === \"firebase\" ? firestoreOk : false,\n persistence: persistence.mode,\n firestoreDetail,\n latencyMs,\n service: \"castle-gateway\",\n wsPort: PORT\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health\") {\n const persistence = getFirebasePersistence();\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n wsPort: PORT,\n persistence: persistence.mode,\n presenceMesh: \"sse\"\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/health\") {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n sendJson(res, 200, {\n ok: true,\n role: \"gateway\",\n queueLagMs: infraMetrics.queueLag,\n queueDepth: infraMetrics.queueDepth,\n errors: infraMetrics.errors,\n status: scored.status,\n score: scored.score,\n reasons: scored.reasons,\n worker: worker\n ? {\n status: worker.status,\n score: worker.score,\n reasons: worker.reasons\n }\n : null,\n timestamp: Date.now()\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisRuntime) {\n const payload = await buildGenesisRuntimeSurfacePayloadLive();\n sendJson(res, 200, payload);\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.genesisIngress) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const actor = await resolveMeshActor(req);\n const clientId = actor.ok ? actor.clientUid : (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestGenesisWorldObservationV0(body, { clientId });\n if (!result.ok) {\n const status =\n result.error === \"rate_limited\" || result.error === \"agent_spoke_throttled\"\n ? 429\n : result.error === \"payload_too_large\"\n ? 413\n : 400;\n sendJson(res, status, result);\n return;\n }\n sendJson(res, result.idempotent ? 200 : 202, result);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisStream) {\n const corsOrigin = accessControlAllowOriginValue(req);\n if (!corsOrigin && String(process.env.NODE_ENV || \"\").toLowerCase() === \"production\") {\n const og = String(req.headers?.origin || \"\").trim();\n if (og) {\n console.warn(\"[GATEWAY] genesis SSE: missing Access-Control-Allow-Origin for browser Origin (CORS whitelist mismatch):\", og);\n }\n }\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n res.write(\"retry: 3000\\n\\n\");\n res.write(\": genesis continuity stream\\n\\n\");\n res.write(\"event: genesis\\n\");\n res.write(`data: {\"booted\":true,\"pid\":${process.pid}}\\n\\n`);\n registerGenesisSseClient(res, req);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLatest) {\n const cp = getLatestGenesisSignedCheckpoint();\n if (!cp) {\n sendJson(res, 404, { ok: false, error: \"no_checkpoint_yet\", hint: \"wait_for_seq_interval_or_configure_signing_secret\" });\n return;\n }\n sendJson(res, 200, { ok: true, checkpoint: cp });\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(rhizohRuntime.routes.genesisCheckpointBySeqPrefix)) {\n const tail = pathname.slice(rhizohRuntime.routes.genesisCheckpointBySeqPrefix.length).split(\"/\")[0];\n const seq = parseInt(String(tail || \"\").trim(), 10);\n const out = await genesisCheckpointQueryBySeqV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"checkpoint_not_found\") {\n sendJson(res, 404, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointRange) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryRangeV0(from, to);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"range_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLineage) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const seq = parseInt(String(u.searchParams.get(\"seq\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryLineageV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"lineage_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisContinuityEvents) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const limit = parseInt(String(u.searchParams.get(\"limit\") || \"\").trim(), 10);\n const out = await queryGenesisContinuityEventArchiveV0(from, to, type, limit);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\" || out.error === \"event_archive_disabled\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"archive_file_too_large\") {\n sendJson(res, 413, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplay) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await resolveGenesisReplayRouterV1({\n from: fromSeq,\n to: toSeq,\n type,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayDiff) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const out = await computeGenesisReplayTemporalDiffV1({ from: fromSeq, to: toSeq, type });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEquivalence) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from1 = parseInt(String(u.searchParams.get(\"from1\") || \"\").trim(), 10);\n const to1 = parseInt(String(u.searchParams.get(\"to1\") || \"\").trim(), 10);\n const from2 = parseInt(String(u.searchParams.get(\"from2\") || \"\").trim(), 10);\n const to2 = parseInt(String(u.searchParams.get(\"to2\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await compareGenesisReplayEquivalenceV1({\n from1,\n to1,\n from2,\n to2,\n type,\n includeCheckpoints\n });\n if (\n !out.ok &&\n (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")\n ) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayAnalytics) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayAnalyticsV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEvolution) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const collapseWindows = parseInt(String(u.searchParams.get(\"collapseWindows\") || \"0\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayEvolutionV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n collapseWindows,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/metrics\") {\n res.writeHead(200, { \"Content-Type\": \"text/plain; version=0.0.4; charset=utf-8\" });\n res.end(renderInfraMetricsProm());\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.infraPrometheusMetrics) {\n if (process.env.CASTLE_PROMETHEUS_METRICS !== \"1\") {\n sendJson(res, 404, { ok: false, error: \"prometheus_metrics_disabled\" });\n return;\n }\n res.writeHead(200, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(renderRhizohPrometheusMetrics());\n return;\n }\n if (req.method === \"GET\" && pathname.startsWith(\"/infra/traces/\")) {\n const traceId = decodeURIComponent(pathname.slice(\"/infra/traces/\".length));\n const item = await getTraceById(traceId);\n if (!item) return sendJson(res, 404, { ok: false, error: \"trace_not_found\" });\n return sendJson(res, 200, { ok: true, trace: item });\n }\n if (req.method === \"GET\" && pathname === \"/infra/traces\") {\n const u = new URL(String(req.url || \"/infra/traces\"), `http://${req.headers.host || \"localhost\"}`);\n const sessionId = String(u.searchParams.get(\"sessionId\") || \"\").trim();\n const items = sessionId ? await listTracesBySession(sessionId) : [];\n return sendJson(res, 200, { ok: true, sessionId, traces: items });\n }\n\n /** Presence room mesh (C): JOIN / LEAVE / SNAPSHOT / DELTA / REPLAY (HTTP); SUBSCRIBE = SSE stream. */\n if (req.method === \"POST\" && pathname === \"/presence/mesh/join\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid, clientUid: actor.clientUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/leave\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshLeave(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/snapshot\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n sendJson(res, 200, meshSnapshot(roomUid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/delta\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, MAX_MESSAGE_BYTES);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const seq = meshAppendDelta(roomUid, actor.clientUid, {\n node: body?.node,\n projectionPatch: body?.projectionPatch,\n writerSubject: body?.writerSubject\n });\n sendJson(res, 200, { ok: true, roomUid, seq });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/replay\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const fromNodeId = body?.fromNodeId != null ? String(body.fromNodeId) : \"\";\n const fromSeq = body?.fromSeq;\n if (fromNodeId) {\n sendJson(res, 200, meshReplayFromNodeId(roomUid, fromNodeId));\n return;\n }\n sendJson(res, 200, meshReplayFromSeq(roomUid, fromSeq));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/presence/mesh/subscribe\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const roomUid = String(q.searchParams.get(\"roomUid\") || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n const write = (chunk) => {\n try {\n res.write(chunk);\n } catch {\n /* closed */\n }\n };\n const unsub = meshSubscribeSse(roomUid, write);\n const onClose = () => {\n unsub();\n meshLeave(roomUid, actor.clientUid);\n try {\n res.end();\n } catch {\n /* ignore */\n }\n };\n req.on(\"close\", onClose);\n req.on(\"aborted\", onClose);\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, uid: auth.uid, mode: auth.mode, items: await listConnections(auth.uid) });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const id = await createConnection(auth.uid, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"create_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n await updateConnection(auth.uid, id, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"update_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"DELETE\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n await deleteConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"delete_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url?.startsWith(\"/rhizoh/network/presence\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const u = new URL(req.url, \"http://localhost\");\n const roomKey = String(u.searchParams.get(\"room\") || \"world_space_c2c_v0\").slice(0, 64);\n sendJson(res, 200, {\n ok: true,\n roomKey,\n presence: listCastleNetworkPresenceV0(roomKey)\n });\n } catch (error) {\n sendJson(res, 500, { ok: false, error: error?.message || \"presence_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/rhizoh/sync/vault\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const snapshot = await getCloudSyncSnapshotV0(auth.uid);\n sendJson(res, 200, { ok: true, uid: auth.uid, snapshot });\n } catch (error) {\n sendJson(res, 500, { ok: false, error: error?.message || \"cloud_sync_pull_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/rhizoh/sync/vault\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const snapshot = await mergeCloudSyncSnapshotV0(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, uid: auth.uid, snapshot });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"cloud_sync_push_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections/test\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n if (!checkHttpRateLimit(`llm_conn_test:${auth.uid}`, RL_LLM_CONN_TEST_PER_MIN, 60_000)) {\n return sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n }\n const hasBodyKey = !!String(payload?.apiKey || \"\").trim();\n const result = await queryRhizohLlm(\n {\n message: String(payload?.message || \"Ping from Castle Gateway\"),\n context: payload?.context || { source: \"connection_test\" },\n provider: payload?.provider,\n model: payload?.model,\n apiKey: payload?.apiKey || \"\"\n },\n { allowExternalApiKey: true, llmKeySource: hasBodyKey ? \"auto\" : \"env\" }\n );\n logLlmAccess({\n route: \"/llm/connections/test\",\n uid: auth.uid,\n llmKeyBillingOwner: result.llmKeyBillingOwner,\n llmKeyOrigin: result.llmKeyOrigin,\n provider: result.provider,\n model: result.model,\n connectionId: null\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"connection_test_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAgentIdentities(auth.uid);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerAgentIdentity({\n ownerUid: auth.uid,\n agentId: payload?.agentId,\n role: payload?.role,\n personaSeed: payload?.personaSeed || {},\n capabilityLevel: payload?.capabilityLevel\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/agents/identities/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const owns = await getAgentIdentity(id);\n if (!owns || owns.ownerUid !== auth.uid) return sendJson(res, 403, { ok: false, error: \"forbidden\" });\n const payload = await readHttpJson(req);\n const row = await updateAgentIdentity(id, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/academy/events\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/academy/events\"}`);\n const status = String(url.searchParams.get(\"status\") || \"\");\n const items = await listAcademyEvents({ ownerUid: auth.uid, status, limit: 120 });\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/academy/events\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const ev = await queueAcademyEvent({\n type: payload?.type,\n roomId: payload?.roomId,\n ownerUid: auth.uid,\n participants: payload?.participants || [],\n topic: payload?.topic || \"academy\",\n payload: payload?.payload || {}\n });\n sendJson(res, 200, { ok: true, event: ev });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && String(req.url || \"\").startsWith(\"/academy/events/\") && String(req.url || \"\").endsWith(\"/resolve\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const parts = String(req.url || \"\").split(\"/\").filter(Boolean);\n const eventId = parts[2] || \"\";\n try {\n const payload = await readHttpJson(req);\n const out = await resolveAcademyEvent(eventId, payload || {});\n sendJson(res, 200, { ok: true, event: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_resolve_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/memory\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/memory\"}`);\n const scope = url.searchParams.get(\"scope\") === \"agents\" ? \"agents\" : \"users\";\n const id = String(url.searchParams.get(\"id\") || (scope === \"agents\" ? \"\" : auth.uid));\n const kind = String(url.searchParams.get(\"kind\") || \"\");\n const limit = Number(url.searchParams.get(\"limit\") || 40);\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n sendJson(res, 200, { ok: true, scope, id, kind, items: await listMemories({ scope, id, kind, limit }) });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const profile = await getPersonaGoalMemory(auth.uid);\n sendJson(res, 200, { ok: true, profile });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const profile = await setPersonaGoalMemory({\n uid: auth.uid,\n personaPatch: payload?.personaPatch || {},\n goals: Array.isArray(payload?.goals) ? payload.goals : null,\n preferencesPatch: payload?.preferencesPatch || {}\n });\n sendJson(res, 200, { ok: true, profile });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"profile_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/compact\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const out = await autoCompactMemories({ scope, id });\n sendJson(res, 200, { ok: true, scope, id, ...out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_compact_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/context\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const context = await getMemoryContext({\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n query: String(payload?.query || \"\"),\n limit: Number(payload?.limit || 80)\n });\n sendJson(res, 200, { ok: true, context });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_context_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/place-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const placeName = String(payload?.placeName || \"\").trim();\n if (!placeName) return sendJson(res, 400, { ok: false, error: \"placeName_required\" });\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const persona = String(payload?.waypointPersona || payload?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(placeName)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Place: ${placeName}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Give a concise conversational briefing about this place for a walking-route companion.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-place-brief\", placeName, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `PLACE_BRIEF:${placeName} => ${String(summary.reply || \"\").slice(0, 900)}`,\n tags: [\"place\", \"brief\", \"event-layer\"],\n importance: 0.62,\n meta: { placeName }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"place-brief\",\n text: `${placeName} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: {\n placeName,\n directive: summary.directive,\n lat: hit?.lat ? Number(hit.lat) : null,\n lon: hit?.lon ? Number(hit.lon) : null\n }\n });\n sendJson(res, 200, { ok: true, place: hit || null, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"place_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/route-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const waypoints = Array.isArray(payload?.waypoints) ? payload.waypoints.slice(0, 24) : [];\n const out = [];\n for (const wp of waypoints) {\n const name = String(wp?.name || \"\").trim();\n if (!name) continue;\n const persona = String(wp?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(name)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Waypoint: ${name}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Generate a concise spoken briefing for route waypoint.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-route-brief\", waypoint: name, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n out.push({\n name,\n persona,\n place: hit || null,\n reply: summary.reply,\n directive: summary.directive\n });\n }\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `ROUTE_BRIEF: waypoints=${out.length} ${out.map((x) => `${x.name}:${x.persona}`).join(\" | \")}`.slice(0, 1300),\n tags: [\"route\", \"waypoint\", \"brief\", \"event-layer\"],\n importance: 0.74,\n meta: { count: out.length }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"route-brief\",\n text: out.map((x) => `${x.name}:${String(x.reply || \"\").slice(0, 140)}`).join(\" | \").slice(0, 1400),\n roomId: \"studio-main\",\n meta: {\n waypointCount: out.length,\n waypoints: out.map((x) => ({\n name: x.name,\n persona: x.persona,\n lat: x.place?.lat ? Number(x.place.lat) : null,\n lon: x.place?.lon ? Number(x.place.lon) : null\n }))\n }\n });\n sendJson(res, 200, { ok: true, items: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"route_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/pdf-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const title = String(payload?.title || \"document\");\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const providedText = String(payload?.text || \"\");\n const url = String(payload?.url || \"\");\n let content = providedText;\n if (!content && url) content = await fetchTextFromUrl(url);\n if (!content) return sendJson(res, 400, { ok: false, error: \"text_or_supported_url_required\" });\n const summary = await queryRhizohLlm(\n {\n message: `Summarize this document and provide key points + practical actions.\\nTitle:${title}\\nContent:\\n${content.slice(0, 12000)}`,\n context: { source: \"event-layer-pdf-brief\", title },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `DOC_BRIEF:${title} => ${String(summary.reply || \"\").slice(0, 1000)}`,\n tags: [\"document\", \"brief\", \"library\", \"event-layer\"],\n importance: 0.7,\n meta: { title, url: url || null }\n });\n await addTranscript(auth.uid, {\n source: \"library-brief\",\n eventType: \"pdf-brief\",\n text: `${title} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { title }\n });\n sendJson(res, 200, { ok: true, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/pdf/upload\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req, 8 * 1024 * 1024);\n const job = await enqueuePdfJob({\n uid: auth.uid,\n fileName: payload?.fileName || \"document.pdf\",\n bytesBase64: payload?.contentBase64\n });\n sendJson(res, 200, { ok: true, job });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_upload_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/session\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createPublishSession(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session\",\n text: `Publish session armed: ${row.protocol} ${row.target}`.slice(0, 500),\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: row.id, bridge: row.bridge }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/studio/publish/sessions\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listPublishSessions(auth.uid, 80);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/health-check\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const protocol = String(payload?.protocol || \"WHIP\");\n const target = String(payload?.target || \"\");\n const health = await checkPublishTargetHealth(protocol, target);\n if (payload?.sessionId) {\n await updatePublishSession(auth.uid, String(payload.sessionId), {\n status: health.ok ? \"healthy\" : \"degraded\",\n reconnectPolicy: {\n enabled: true,\n backoffSec: [2, 5, 10, 20],\n nextRetryInSec: health.reconnectInSec || 0,\n maxRetries: 8,\n reason: health.reason\n }\n });\n }\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-health\",\n text: `${protocol} ${target} => ${health.ok ? \"healthy\" : \"degraded\"} (${health.reason})`,\n roomId: \"studio-main\",\n meta: { protocol, target, reconnect: !!health.shouldReconnect }\n });\n sendJson(res, 200, { ok: true, health });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_health_check_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/studio/publish/sessions/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updatePublishSession(auth.uid, id, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session-update\",\n text: `Session ${id} -> ${row.status || \"updated\"}`,\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: id, status: row.status, ingestUrl: row.ingestUrl || \"\" }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/transcripts\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/transcripts\"}`);\n const limit = Math.max(1, Math.min(200, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await listTranscripts(auth.uid, limit);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/transcripts\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await addTranscript(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"transcript_write_failed\" });\n }\n return;\n }\n\n /** E1: broadcast intent → GreenRoom capability → transcript + memory artifact */\n if (req.method === \"POST\" && req.url === \"/studio/capabilities/greenroom\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const traceId = String(payload?.traceId || `TRC-${Date.now().toString(36).toUpperCase()}`);\n const title = String(payload?.title || \"Castle Live Broadcast\").slice(0, 200);\n const intentRaw = String(payload?.intentRaw || \"\").slice(0, 800);\n const audienceEstimate = Math.max(0, Math.min(50_000_000, Number(payload?.audienceEstimate) || 1200));\n const roomId = String(payload?.roomId || \"greenroom-main\");\n const transcriptRow = await addTranscript(auth.uid, {\n source: \"rhizoh\",\n eventType: \"BROADCAST_ROUTED\",\n text: `GreenRoom · ${title}${intentRaw ? `\\nIntent: ${intentRaw}` : \"\"}`,\n roomId,\n meta: {\n ack: \"BROADCAST_ROUTED\",\n capability: \"greenroom\",\n intentType: \"broadcast\",\n traceId,\n audienceEstimate,\n artifact: {\n kind: \"broadcast_card\",\n title,\n traceId,\n roomId,\n createdAt: Date.now()\n }\n }\n });\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n text: `GREENROOM_BROADCAST:${title} trace=${traceId}`,\n tags: [\"broadcast\", \"greenroom\", \"artifact\", \"castle-library\"],\n importance: 0.72,\n kind: \"episodic\",\n meta: { traceId, capability: \"greenroom\", transcriptId: transcriptRow.id }\n });\n sendJson(res, 200, {\n ok: true,\n ack: \"BROADCAST_ROUTED\",\n traceId,\n transcript: transcriptRow,\n replayPath: `/replay/${encodeURIComponent(traceId)}`,\n sharePath: `/replay/${encodeURIComponent(traceId)}`\n });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"greenroom_capability_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/metadata/search\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/metadata/search\"}`);\n const q = String(url.searchParams.get(\"q\") || \"\");\n const limit = Math.max(1, Math.min(300, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await searchMetadata(auth.uid, q, limit);\n sendJson(res, 200, { ok: true, q, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerDevice(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, device: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"device_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const lat = Number(payload?.lat);\n const lon = Number(payload?.lon);\n const speed = Math.max(0, Number(payload?.speed || 0));\n const cfgCenterLat = Number(process.env.CASTLE_GEOFENCE_CENTER_LAT || 41.0082);\n const cfgCenterLon = Number(process.env.CASTLE_GEOFENCE_CENTER_LON || 28.9784);\n const cfgRadiusM = Math.max(100, Number(process.env.CASTLE_GEOFENCE_RADIUS_M || 55000));\n const cfgSpeedCap = Math.max(1, Number(process.env.CASTLE_SPEED_CAP_MPS || 35));\n const dist = Number.isFinite(lat) && Number.isFinite(lon) ? haversineMeters(cfgCenterLat, cfgCenterLon, lat, lon) : 0;\n const geofenceOk = !Number.isFinite(dist) ? true : dist <= cfgRadiusM;\n const speedOk = speed <= cfgSpeedCap;\n const failsafe = !(geofenceOk && speedOk);\n const safety = {\n geofenceOk,\n speedOk,\n failsafe,\n distanceFromCenterM: Number.isFinite(dist) ? dist : null,\n speedCapMps: cfgSpeedCap\n };\n const row = await appendTelemetry(auth.uid, deviceId, {\n lat,\n lon,\n speed,\n battery: payload?.battery,\n mode: payload?.mode || (failsafe ? \"FAILSAFE_HOLD\" : \"ACTIVE\"),\n raw: payload?.raw || {}\n });\n await updateDevice(auth.uid, deviceId, {\n status: failsafe ? \"failsafe\" : \"active\",\n lastTelemetryAt: row.ts,\n safetyState: safety\n });\n if (failsafe) {\n await addTranscript(auth.uid, {\n source: \"robotics-guardrails\",\n eventType: \"failsafe\",\n text: `Device ${deviceId} failsafe triggered geofenceOk=${geofenceOk} speedOk=${speedOk}`,\n roomId: \"studio-main\",\n meta: { deviceId, safety }\n });\n }\n sendJson(res, 200, { ok: true, telemetry: row, safety, command: failsafe ? \"FAILSAFE_HOLD\" : \"CONTINUE\" });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"telemetry_ingest_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listTelemetry(auth.uid, 150);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createAutomation(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"automation-scheduled\",\n text: `Automation scheduled: ${row.title} at ${new Date(row.scheduleAt).toLocaleString()}`,\n roomId: \"studio-main\",\n meta: { automationId: row.id, action: row.action, deviceId: row.deviceId }\n });\n sendJson(res, 200, { ok: true, automation: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"automation_create_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAutomations(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listDevices(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const devs = await listDevices(auth.uid, 500);\n const d = devs.find((x) => x.id === deviceId);\n if (!d) return sendJson(res, 404, { ok: false, error: \"device_not_found\" });\n const action = String(payload?.action || \"\");\n const caps = Array.isArray(d.capabilityProfile) ? d.capabilityProfile : [];\n if (action && caps.length && !caps.includes(action) && !caps.includes(\"*\")) {\n return sendJson(res, 400, { ok: false, error: \"unsupported_action_for_device\", capabilityProfile: caps });\n }\n const cmd = await enqueueCommand(auth.uid, {\n deviceId,\n action: action || \"NOOP\",\n params: payload?.params || {},\n adapter: payload?.adapter || d.adapter || \"websocket\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-command-queue\",\n eventType: \"command-queued\",\n text: `${cmd.deviceId} -> ${cmd.action}`,\n roomId: \"studio-main\",\n meta: { commandId: cmd.id, adapter: cmd.adapter }\n });\n sendJson(res, 200, { ok: true, command: cmd });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listCommands(auth.uid, 200);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/robotics/commands/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updateCommandStatus(auth.uid, id, payload?.status || \"sent\");\n sendJson(res, 200, { ok: true, command: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/rhizoh/bridge\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const result = await runRhizohBrainV2({\n message: String(payload?.message || \"robotics bridge command\"),\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n worldState: payload?.worldState || {},\n deviceState: payload?.deviceState || {},\n intent: \"robotics-mechanics\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-bridge\",\n eventType: \"robotics-command\",\n text: `Bridge -> ${String(result.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { directive: result.directive, agentId: payload?.agentId || \"\" }\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"robotics_bridge_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/planner/graph\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const graph = buildTaskActionGraph(String(payload?.task || \"\"), payload?.context || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"planner-graph\",\n text: `Planner graph built: ${graph.taskTitle} nodes=${graph.nodes.length}`,\n roomId: \"studio-main\",\n meta: { taskTitle: graph.taskTitle, nodeCount: graph.nodes.length }\n });\n sendJson(res, 200, { ok: true, graph });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"planner_graph_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerSocialChannel(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, channel: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_channel_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listSocialChannels(auth.uid, 100);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/broadcast\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const text = String(payload?.text || \"\").slice(0, 1200);\n const channels = await listSocialChannels(auth.uid, 100);\n const enabled = channels.filter((c) => c.status === \"enabled\");\n const deliveries = [];\n for (const c of enabled) {\n const out = await deliverSocialWithRetry(c, text);\n deliveries.push({ id: c.id, platform: c.platform, ...out });\n }\n const okCount = deliveries.filter((d) => d.ok).length;\n await addTranscript(auth.uid, {\n source: \"social-layer\",\n eventType: \"social-broadcast\",\n text: `Broadcast delivered ${okCount}/${enabled.length} channel(s): ${text.slice(0, 240)}`,\n roomId: \"studio-main\",\n meta: { deliveries }\n });\n sendJson(res, 200, { ok: true, sentChannels: okCount, totalChannels: enabled.length, deliveries });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_broadcast_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/ethics/programs\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, ...buildEthicsPrograms() });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands/dispatch\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const all = await listCommands(auth.uid, 200);\n const queued = all.filter((c) => String(c.status || \"queued\") === \"queued\").slice(0, 50);\n const devices = await listDevices(auth.uid, 500);\n const byId = new Map(devices.map((d) => [d.id, d]));\n const results = [];\n for (const cmd of queued) {\n const dev = byId.get(cmd.deviceId);\n if (!dev) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: \"device_not_found\" });\n continue;\n }\n try {\n const dispatched = await dispatchCommandToAdapter(dev, cmd);\n await updateCommandStatus(auth.uid, cmd.id, \"sent\");\n results.push({ id: cmd.id, ok: true, adapter: dispatched.adapter, via: dispatched.via });\n } catch (error) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: String(error?.message || \"dispatch_failed\") });\n }\n }\n await addTranscript(auth.uid, {\n source: \"robotics-command-dispatch\",\n eventType: \"command-dispatch-batch\",\n text: `Dispatch batch size=${results.length} ok=${results.filter((x) => x.ok).length}`,\n roomId: \"studio-main\",\n meta: { results: results.slice(0, 30) }\n });\n sendJson(res, 200, { ok: true, processed: results.length, results });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"dispatch_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/pdf/jobs/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n const job = await getPdfJob(auth.uid, id);\n if (!job) return sendJson(res, 404, { ok: false, error: \"pdf_job_not_found\" });\n sendJson(res, 200, { ok: true, job });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const row = await appendMemory({\n scope,\n id,\n text: payload?.text,\n tags: payload?.tags || [],\n importance: payload?.importance,\n kind: payload?.kind || \"episodic\",\n meta: { uid: auth.uid, ...(payload?.meta || {}) }\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.externalTruth) {\n try {\n if (process.env.RHIZOH_EXTERNAL_TRUTH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_truth:${ip}`, RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n sendJson(res, 200, buildRhizohExternalGroundTruthPayload(req));\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.productOutcome) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const rawBody = await readHttpBodyText(req, 64 * 1024);\n const body = rawBody.trim() ? safeJsonParse(rawBody) : null;\n if (!body || typeof body !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"invalid_json_payload\" });\n return;\n }\n const sigHdr = String(req.headers?.[\"x-rhizoh-outcome-signature\"] || \"\").trim();\n const hmacV = verifyRhizohOutcomeHmac(rawBody, sigHdr);\n if (!hmacV.ok) {\n sendJson(res, 401, { ok: false, error: hmacV.error || \"outcome_hmac_invalid\" });\n return;\n }\n const srcTokHdr = String(req.headers?.[\"x-rhizoh-outcome-source-token\"] || \"\").trim();\n const srcV = verifyRhizohOutcomeSourceToken(body.source, srcTokHdr);\n if (!srcV.ok) {\n sendJson(res, 401, { ok: false, error: srcV.error || \"outcome_source_token_invalid\" });\n return;\n }\n const subjectKey = String(body.subjectKey || \"\").trim().slice(0, 128);\n if (\n subjectKey &&\n !checkHttpRateLimit(\n `rhizoh_product_outcome_subj:${subjectKey}`,\n RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN,\n 60_000\n )\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_subject\" });\n return;\n }\n const result = ingestRhizohProductOutcomeHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n schemaVersion: \"1.1.0\",\n aggregate: result.aggregate,\n integrity: result.integrity\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"outcome_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.productOutcomeAggregate) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome_agg:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const u = new URL(String(req.url || \"/\"), \"http://localhost\");\n const cohortId = u.searchParams.get(\"cohortId\") || \"\";\n const decisionFingerprint = u.searchParams.get(\"decisionFingerprint\") || \"\";\n const limit = u.searchParams.get(\"limit\") || \"50\";\n const rows = listRhizohProductOutcomeAggregates({\n cohortId: cohortId || undefined,\n decisionFingerprint: decisionFingerprint || undefined,\n limit: Number(limit) || 50\n });\n sendJson(res, 200, { ok: true, schemaVersion: \"1.1.0\", rows });\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.externalLossBatch) {\n try {\n if (process.env.RHIZOH_EXTERNAL_LOSS_BATCH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_loss_batch:${ip}`, RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const result = ingestRhizohExternalLossBatchHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, { ok: true, accepted: result.accepted ?? 0 });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"batch_failed\") });\n }\n return;\n }\n\n function moderationAdminOk(req) {\n const expected = String(process.env.CASTLE_MODERATION_ADMIN_KEY || \"\").trim();\n if (!expected) return false;\n const got = String(req.headers[\"x-castle-moderation-key\"] || \"\").trim();\n return got.length > 0 && got === expected;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.opsAbuseReport) {\n try {\n const body = await readHttpJson(req, 32 * 1024);\n const auth = await resolveHttpUser(req);\n const entry = submitAbuseReportV0({\n reporterUid: auth.ok ? auth.uid : null,\n targetUid: body?.targetUid ?? null,\n category: body?.category,\n detail: body?.detail,\n traceId: body?.traceId ?? null\n });\n sendJson(res, 202, { ok: true, reportId: entry.id, status: entry.status });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsModerationQueue) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n queue: listModerationQueueV0(100),\n softBlocked: listSoftBlockedUsersV0()\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-block\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const out = softBlockUserV0(body?.uid, body?.reason);\n sendJson(res, out.ok ? 200 : 400, out);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-unblock\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n sendJson(res, 200, removeSoftBlockV0(body?.uid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsAgentSnapshots) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n const limit = Number(new URL(req.url || \"\", \"http://local\").searchParams.get(\"limit\") || 50);\n sendJson(res, 200, { ok: true, snapshots: listRecentAgentSnapshotsV0(limit) });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.academicObservatoryExport) {\n const auth = await resolveHttpUser(req);\n const out = handleAcademicObservatoryExportGetV0(req, auth);\n sendJson(res, out.status, out.body);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsHardeningStatus) {\n const auth = await resolveHttpUser(req);\n const principal = auth.ok ? `uid:${auth.uid}` : null;\n const gclSnap = await getGlobalCostLedgerSnapshotV0(principal);\n const economicStrategy = evaluateEconomicStrategyL3V0(gclSnap);\n const hardeningTenantId = resolveHardeningTenantIdV0(auth, req);\n const unifiedState = await buildUnifiedStateNarrativeV0({\n tenantId: hardeningTenantId ?? undefined,\n platformScope: !hardeningTenantId,\n principal\n });\n sendJson(res, 200, {\n ok: true,\n schema: \"rhizoh.ops.hardening_status.v0\",\n containment: readAgentContainmentConfigV0(),\n cost: readCostContainmentConfigV0(),\n gcl: {\n backend: await getGclBackendStatusV0(),\n snapshot: gclSnap\n },\n economicStrategyL3: economicStrategy,\n phasedRollout: await getPhasedRolloutStatsV0(),\n tier: readPhasedRolloutTierV0(),\n principalCost: auth.ok ? await getCostContainmentStatsV0(principal) : null,\n stressTaxonomy: verifyStressTaxonomyCoverageV0(),\n stressConflictResolution: verifyStressConflictResolutionV0(),\n resolutionStabilityEnvelope: verifyResolutionStabilityEnvelopeV0(),\n unifiedState: {\n tenantScope: {\n tenantId: unifiedState.tenantScope?.tenantId,\n scope: unifiedState.tenantScope?.scope,\n noGlobalDerivedState: unifiedState.tenantScope?.noGlobalDerivedState\n },\n narrativeFingerprint: unifiedState.narrativeFingerprint,\n screenshotScopeWatermark: unifiedState.screenshotScopeWatermark,\n interpretationSafetyContract: unifiedState.interpretationSafetyContract,\n interpretationUxContract: {\n schema: unifiedState.interpretationUxContract?.schema,\n layerOrder: unifiedState.interpretationUxContract?.layerOrder,\n narrativeNeverVisuallyDominant: unifiedState.interpretationUxContract?.narrativeNeverVisuallyDominant\n },\n governance: {\n narrativeIsNotDecisionLayer: unifiedState.governance.narrativeIsNotDecisionLayer,\n displayOrder: unifiedState.governance.displayOrder,\n trustPosture: unifiedState.governance.trustPosture,\n interpretationUxContract: unifiedState.governance.interpretationUxContract?.schema\n },\n humanOps: {\n decisionTier: unifiedState.humanOps?.humanDecisionScaling?.tier,\n decisionOwner: unifiedState.humanOps?.humanDecisionScaling?.decisionOwner,\n opsRouteId: unifiedState.humanOps?.humanDecisionOpsRunbook?.routing?.routeId,\n slaAckMinutes: unifiedState.humanOps?.humanDecisionOpsRunbook?.sla?.ackMinutes,\n uiDisclaimerRequired: unifiedState.humanOps?.narrativeUiSafety?.displayRules?.requireDisclaimerBanner,\n misreadHighResidual: unifiedState.humanOps?.misreadSimulation?.highResidualCount,\n propagationHighResidual: unifiedState.humanOps?.socialPropagationSimulation?.highResidualCount,\n worstWatermarkSurvivability:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.worstWatermarkSurvivability,\n dominantDistortionSource:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.dominantDistortionSource\n },\n culturalRisk: {\n perceptualRiskClass: unifiedState.culturalRisk?.perceptualRiskClass,\n trustFork: unifiedState.culturalRisk?.trustDynamics?.fork,\n trustDecayScore: unifiedState.culturalRisk?.trustDynamics?.scores?.trustDecay,\n mythologyScore: unifiedState.culturalRisk?.trustDynamics?.scores?.mythology,\n confidenceDetachmentExposure:\n unifiedState.culturalRisk?.trustDynamics?.axes?.confidenceDetachmentExposure\n },\n appliedSystemsLayer: {\n schema: unifiedState.appliedSystemsLayer?.schema,\n researchOutputType: unifiedState.appliedSystemsLayer?.researchGovernance?.outputType,\n roboticsBlockActuation: unifiedState.appliedSystemsLayer?.roboticsGrounding?.blockActuation,\n spiralRumorCount:\n unifiedState.appliedSystemsLayer?.spiralMMOGameKernel?.gameLayer?.rumors?.length\n },\n actionSemanticGovernance: {\n schema: unifiedState.actionSemanticGovernance?.schema,\n highRiskActionCount: unifiedState.actionSemanticGovernance?.highRiskActionCount,\n loopClosed: unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.loopClosed,\n authorityCollapseMitigated:\n unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.risks?.authorityCollapse\n ?.mitigated\n },\n actionContextResolution: {\n schema: unifiedState.actionContextResolution?.schema,\n contextFingerprint: unifiedState.actionContextResolution?.contextFingerprint?.fingerprintHash,\n domainCollision: unifiedState.actionContextResolution?.domainCollision?.detected,\n deployAgentSpiralBlocked:\n unifiedState.actionContextResolution?.actionOverloadingRisk?.deployAgentExample\n ?.spiral_mmo === \"blocked\",\n roboticsActuationEligible:\n unifiedState.actionContextResolution?.roboticsActuationCorrectness?.actuationEligible\n },\n epistemicCoherence: {\n schema: unifiedState.epistemicCoherence?.schema,\n verdict: unifiedState.epistemicCoherence?.systemCoherence?.verdict,\n coherenceScore: unifiedState.epistemicCoherence?.systemCoherence?.score,\n contradictionCount: unifiedState.epistemicCoherence?.crossLayerContradictions?.count,\n operatorDecision: unifiedState.epistemicCoherence?.uxCompression?.operatorDecision,\n headlineTr: unifiedState.epistemicCoherence?.uxCompression?.headline?.tr,\n falseSafetyWarning: Boolean(\n unifiedState.epistemicCoherence?.uxCompression?.falseSafetyWarning\n )\n },\n coherenceAuthorityBoundary: {\n schema: unifiedState.coherenceAuthorityBoundary?.schema,\n bindingValid: unifiedState.coherenceAuthorityBoundary?.eclAuthorityBinding?.valid,\n canExecute: unifiedState.coherenceAuthorityBoundary?.contract?.can_execute,\n mandatoryBannerTr:\n unifiedState.coherenceAuthorityBoundary?.disclaimers?.mandatoryBanner?.tr\n },\n decisionLatencyGovernance: {\n schema: unifiedState.decisionLatencyGovernance?.schema,\n latencyTier: unifiedState.decisionLatencyGovernance?.latencyTier,\n inflationScore: unifiedState.decisionLatencyGovernance?.latencyInflation?.score,\n fastPathEligible: unifiedState.decisionLatencyGovernance?.fastPath?.eligible,\n primaryActionId: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.primaryActionId,\n ackSlaMinutes: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.ackSlaMinutes,\n certaintyCap: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.certaintyCap,\n uncertaintyItems:\n unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.uncertaintyEnvelope?.itemCount\n },\n decisionPacketUncertaintyBoundary: {\n schema: unifiedState.decisionPacketUncertaintyBoundary?.schema,\n invariantValid: unifiedState.decisionPacketUncertaintyBoundary?.invariantCheck?.valid,\n centralInvariant: unifiedState.decisionPacketUncertaintyBoundary?.centralInvariant\n },\n epistemicDecisionPacing: {\n schema: unifiedState.epistemicDecisionPacing?.schema,\n queueSaturationTier:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation?.tier,\n operatorProcessingLatency:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation\n ?.operatorProcessingLatency,\n postureWindowCount:\n unifiedState.epistemicDecisionPacing?.temporaryStaticPostureWindows\n ?.temporaryStaticPostureWindows?.windowCount,\n executionBoundariesValid:\n unifiedState.epistemicDecisionPacing?.executionBoundaries?.valid,\n grantsExecutionApproval: unifiedState.epistemicDecisionPacing?.grantsExecutionApproval\n },\n epistemicTemporalCoherence: {\n schema: unifiedState.epistemicTemporalCoherence?.schema,\n allInvariantsValid: unifiedState.epistemicTemporalCoherence?.allInvariantsValid,\n temporalAlignmentValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.temporalAlignment?.valid,\n crossWindowGuardValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard?.valid,\n delayedTruthValid: unifiedState.epistemicTemporalCoherence?.invariants?.delayedTruthRule?.valid,\n reconciliationRequired:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard\n ?.reconciliation?.required\n },\n realityDriftObserver: {\n schema: unifiedState.realityDriftObserver?.schema,\n driftActive: unifiedState.realityDriftObserver?.driftObserverSummary?.active,\n misapprehensionShapeCount:\n unifiedState.realityDriftObserver?.misapprehensionShapeCatalog?.shapeCount,\n criticalShapeCount:\n unifiedState.realityDriftObserver?.driftObserverSummary?.criticalShapeCount,\n roboticsMismatchActive:\n unifiedState.realityDriftObserver?.roboticsFeedbackMismatch?.active\n },\n driftCausality: {\n schema: unifiedState.driftCausality?.schema,\n shapesWithCausalExplanation: unifiedState.driftCausality?.rdolLinkage?.shapesWithCausalExplanation,\n activeDomainCount: unifiedState.driftCausality?.causalitySummary?.activeDomainCount,\n propagationCausalityActive: unifiedState.driftCausality?.causalDomains?.propagation?.active\n },\n productReadiness: unifiedState.productReadiness,\n stateLayers: {\n raw: unifiedState.stateLayers.raw,\n derived: {\n systemState: unifiedState.stateLayers.derived.systemState,\n narrative: unifiedState.stateLayers.derived.narrative,\n validation: unifiedState.stateLayers.derived.validation\n },\n policy: {\n binding: unifiedState.stateLayers.policy.binding,\n decisionOwner: unifiedState.stateLayers.policy.decisionOwner,\n prohibitedActions: unifiedState.stateLayers.policy.prohibitedActions\n }\n },\n systemState: unifiedState.systemState,\n interpretation: unifiedState.interpretation,\n validation: {\n trustPosture: unifiedState.validation.trustPosture,\n adjustedConfidence: unifiedState.validation.adjustedConfidence,\n agreement: unifiedState.validation.confidenceDecomposition.agreement,\n divergenceScore: unifiedState.validation.divergence.divergenceScore,\n validated: unifiedState.validation.divergence.validated,\n uncertaintyTags: unifiedState.validation.uncertainty.tags.map((t) => t.id),\n flags: unifiedState.validation.divergence.flags.map((f) => f.id)\n },\n compression: unifiedState.compression\n }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 6 * 1024 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (\n !checkHttpRateLimit(`rhizoh_voice_transcribe:${rlKey}`, RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN, 60_000)\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const voiceEnv = rhizohVoiceTranscribeEnvV3();\n if (!voiceEnv.any) {\n sendJson(res, 503, {\n ok: false,\n error: \"voice_asr_not_configured\",\n env: voiceEnv\n });\n return;\n }\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n const result = await runRhizohVoiceTranscribeV3(\n applyLanguagePropagationToVoiceBodyV1(payload, langProp)\n );\n sendJson(res, result.ok ? 200 : 422, {\n ...result,\n traceId: String(payload?.traceId || \"\"),\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n atMs: Date.now()\n });\n } catch (error) {\n sendJson(res, 500, {\n ok: false,\n error: \"voice_transcribe_failed\",\n detail: String(error?.message || error),\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(\"/rhizoh/llm/task/\")) {\n try {\n const rawTaskId = decodeURIComponent(pathname.slice(\"/rhizoh/llm/task/\".length).split(\"/\")[0] || \"\");\n const taskId = String(rawTaskId || \"\").toLowerCase() === \"last\" ? getLatestLlmWorkerTaskIdV0() : rawTaskId;\n const snap = taskId ? getLlmWorkerTaskSnapshotV0(taskId) : null;\n if (!snap) {\n sendJson(res, 404, {\n ok: false,\n error: \"llm_task_not_found\",\n taskId: rawTaskId,\n status: \"missing\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n if (snap.status === \"processing\") {\n sendJson(res, 202, {\n ok: true,\n taskId: snap.taskId,\n status: \"processing\",\n createdAt: snap.createdAt,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n // Terminal states (completed/failed) always HTTP 200 — poll must not look like server fault (500).\n sendJson(res, 200, {\n ...(snap.body || { ok: false, error: \"llm_task_empty\", taskId: snap.taskId }),\n taskId: snap.taskId,\n status: snap.status === \"completed\" ? \"completed\" : snap.status,\n terminal: true,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n } catch (error) {\n console.error(\"[llm-task-poll] handler failed:\", error);\n sendJson(res, 200, {\n ok: false,\n error: \"llm_task_poll_handler_failed\",\n detail: String(error?.message || error),\n status: \"failed\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === rhizohRuntime.routes.rhizohLlm) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 128 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n logRhizohHealth(\"gateway_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: String(payload?.traceId || \"\"),\n auth: auth.ok ? \"ok\" : \"anon\",\n ip\n });\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (!checkHttpRateLimit(`rhizoh_llm:${rlKey}`, RL_RHIZOH_LLM_PER_MIN, 60_000)) {\n const rlTaxonomy = classifyStressResponseV0({ code: \"rate_limit_exceeded\" });\n return sendJson(res, 429, {\n ok: false,\n error: \"rate_limit_exceeded\",\n reply: rlTaxonomy.userMessageTr || \"İstek sınırı aşıldı. Kısa süre sonra tekrar deneyin.\",\n stressClass: rlTaxonomy.stressClass,\n responseAction: rlTaxonomy.responseAction,\n stressMatrix: rlTaxonomy.matrix,\n stressInterpretable: rlTaxonomy.interpretable,\n stressConfidence: rlTaxonomy.stressConfidence,\n actionConfidence: rlTaxonomy.actionConfidence,\n actionSoftened: rlTaxonomy.actionSoftened,\n responseActionStrict: rlTaxonomy.responseActionStrict,\n actionInterpretable: rlTaxonomy.actionInterpretable,\n conflictResolution: rlTaxonomy.conflictResolution,\n directive: \"NONE\"\n });\n }\n\n const requireExplicit = process.env.CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE === \"1\";\n const keyMode = normalizeClientLlmKeySource(payload?.llmKeySource ?? payload?.keySource);\n if (keyMode === null) {\n return sendJson(res, 400, {\n ok: false,\n error: \"invalid_llm_key_source\",\n reply: \"llmKeySource geçersiz. Kullanın: env | user_connection | auto\",\n directive: \"NONE\"\n });\n }\n if (requireExplicit && keyMode === \"auto\") {\n return sendJson(res, 400, {\n ok: false,\n error: \"llm_key_source_required\",\n reply: \"Sunucu CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE=1: istekte llmKeySource (env veya user_connection) zorunlu.\",\n directive: \"NONE\"\n });\n }\n\n const conn = auth.ok ? await resolveConnection(auth.uid, String(payload?.connectionId || \"\")) : null;\n\n if (keyMode === \"user_connection\") {\n if (!auth.ok) {\n return sendJson(res, 401, {\n ok: false,\n error: \"llm_auth_required_for_user_connection\",\n reply: \"Kayıtlı LLM bağlantısı için giriş gerekli.\",\n directive: \"NONE\"\n });\n }\n if (!conn?.apiKey) {\n return sendJson(res, 400, {\n ok: false,\n error: \"user_llm_connection_required\",\n reply: \"Geçerli bir LLM bağlantısı yok; Studio’da bağlantı ekleyin veya llmKeySource=env kullanın.\",\n directive: \"NONE\"\n });\n }\n }\n\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n let safePayload = applyLanguagePropagationToLlmPayloadV1({ ...(payload || {}) }, langProp);\n const trimOut = trimRhizohLlmGatewayPayloadV1(safePayload);\n if (trimOut.meta.trimmed) {\n logRhizohHealth(\"llm_payload_trimmed\", {\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n bytesBefore: trimOut.meta.bytesBefore,\n bytesAfter: trimOut.meta.bytesAfter\n });\n }\n safePayload = trimOut.payload;\n delete safePayload.apiKey;\n delete safePayload.llmKeySource;\n delete safePayload.keySource;\n\n const llmMessage = String(\n safePayload?.message || safePayload?.text || payload?.message || payload?.text || \"\"\n ).trim();\n if (!llmMessage) {\n return sendJson(res, 400, {\n ok: false,\n error: \"message_required\",\n reply: \"Mesaj metni gerekli (message alanı boş).\",\n directive: \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n if (!safePayload.message) {\n safePayload = { ...safePayload, message: llmMessage };\n }\n\n let resolvedProvider = payload?.provider;\n let resolvedModel = payload?.model;\n let connApiKey = \"\";\n\n ({\n resolvedProvider,\n resolvedModel,\n connApiKey\n } = resolveServerLlmProviderV0(keyMode, conn, payload));\n\n const turnInput = {\n safePayload,\n auth,\n keyMode,\n conn,\n resolvedProvider,\n resolvedModel,\n connApiKey\n };\n const turnMeta = {\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n langEcho: buildRhizohLanguagePropagationEchoV1(langProp)\n };\n\n if (\n isLlmWorkerEnabledV0() &&\n isLlmWorkerAsyncEnabledV0() &&\n String(req.headers[\"x-rhizoh-llm-sync\"] || \"\").trim() !== \"1\"\n ) {\n const taskId = enqueueLlmWorkerTaskAsyncV0(turnInput, turnMeta);\n logRhizohHealth(\"llm_worker_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n taskId,\n traceId: String(payload?.traceId || taskId),\n keyMode,\n async: true\n });\n sendJson(res, 202, {\n ok: true,\n taskId,\n status: \"processing\",\n traceId: String(payload?.traceId || taskId),\n pollPath: `/rhizoh/llm/task/${taskId}`,\n gatewayPid: process.pid,\n ...turnMeta.langEcho\n });\n return;\n }\n\n const { result, traceId, turnLatencyMs, spinePhases, sampledTrace } = isLlmWorkerEnabledV0()\n ? await runLlmWorkerTaskV0(turnInput, turnMeta)\n : await rhizohGatewayTurn(turnInput);\n logRhizohHealth(\"llm_response\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId,\n turnLatencyMs: Number(turnLatencyMs || 0),\n keyMode\n });\n sendJson(res, 200, {\n ...result,\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n traceId,\n turnLatencyMs,\n ...(spinePhases ? { spinePhases } : {}),\n ...(sampledTrace !== undefined ? { sampledTrace } : {})\n });\n } catch (error) {\n const msg = String(error?.message || \"\");\n const code = error?.code || \"\";\n let status = 500;\n if (msg === \"payload_too_large\") status = 413;\n else if (msg === \"rate_limit_exceeded\") status = 429;\n else if (\n code === \"cost_hard_limit\" ||\n code === \"phased_rollout_capacity\" ||\n error?.containment === true ||\n String(code).startsWith(\"agent_\")\n ) {\n status = 429;\n } else if (code === \"user_soft_blocked\" || code === \"prompt_abuse_detected\") status = 403;\n else if (\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(code) ||\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(msg) ||\n msg.includes(\"missing_api_key_for_\")\n )\n status = 400;\n let rhizohFailureKind = \"provider_error\";\n if (msg === \"rate_limit_exceeded\" || status === 429) rhizohFailureKind = \"rate_limit\";\n else if (status === 403) rhizohFailureKind = \"policy_block\";\n else if (status === 400 || status === 413) rhizohFailureKind = \"client_config\";\n else if (msg.startsWith(\"provider_http_\")) {\n const st = Number(msg.replace(\"provider_http_\", \"\"));\n if (st === 408 || st === 504) rhizohFailureKind = \"timeout\";\n else rhizohFailureKind = \"provider_error\";\n } else if (/timeout|timed out|ETIMEDOUT/i.test(msg)) rhizohFailureKind = \"timeout\";\n\n const providerHttpMatch = msg.match(/^provider_http_(\\d{3})$/);\n const stressTaxonomy =\n error?.stressClass != null\n ? {\n stressClass: error.stressClass,\n responseAction: error.responseAction,\n stressMatrix: error.stressMatrix,\n stressInterpretable: error.stressInterpretable !== false,\n stressConfidence: error.stressConfidence,\n actionConfidence: error.actionConfidence,\n actionSoftened: error.actionSoftened,\n responseActionStrict: error.responseActionStrict,\n actionInterpretable: error.actionInterpretable,\n conflictResolution: error.conflictResolution,\n stressSecondary: error.stressSecondary,\n responseActions: error.responseActions\n }\n : (() => {\n const t = classifyStressResponseV0({\n code: code || msg,\n rhizohFailureKind,\n providerHttpStatus: providerHttpMatch\n ? Number(providerHttpMatch[1])\n : null\n });\n return {\n stressClass: t.stressClass,\n responseAction: t.responseAction,\n stressMatrix: t.matrix,\n stressInterpretable: t.interpretable,\n stressConfidence: t.stressConfidence,\n actionConfidence: t.actionConfidence,\n actionSoftened: t.actionSoftened,\n responseActionStrict: t.responseActionStrict,\n actionInterpretable: t.actionInterpretable,\n conflictResolution: t.conflictResolution,\n stressSecondary: t.stressSecondary,\n responseActions: t.responseActions\n };\n })();\n sendJson(res, status, {\n ok: false,\n rhizohFailureKind,\n ...(providerHttpMatch ? { providerHttpStatus: Number(providerHttpMatch[1]) } : {}),\n error: code || msg || \"rhizoh_llm_failed\",\n detail: msg || code || undefined,\n reply:\n error?.reply ||\n (status === 500 ? \"Rhizoh bağlantısı geçici olarak kesildi.\" : msg),\n directive: error?.directive || \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n ...stressTaxonomy\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicSeal) {\n try {\n if (!EPISTEMIC_SEAL_SECRET) {\n sendJson(res, 503, { ok: false, error: \"epistemic_seal_disabled\" });\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`epistemic_seal:${ip}`, RL_EPISTEMIC_SEAL_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const truth = body.truth_contract;\n if (!truth || typeof truth !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"truth_contract_required\" });\n return;\n }\n const sealBody = {\n truth_contract: truth,\n runtime_hash: String(body.runtime_hash || \"\"),\n model_route: {\n provider: body.model_route?.provider ?? null,\n model: body.model_route?.model ?? null\n },\n memory_digest: String(body.memory_digest || \"\"),\n world_snapshot_hash: String(body.world_snapshot_hash || \"\"),\n timestamp: Number(body.timestamp) || Date.now()\n };\n const canonical = canonicalEpistemicSealString(sealBody);\n const { hash, signature } = hashAndSignEpistemicSeal(canonical, EPISTEMIC_SEAL_SECRET);\n recordGenesisEpistemicSealIssued(hash);\n const attestation = {\n schema: EPISTEMIC_SEAL_SCHEMA,\n algorithm: \"SHA-256+HMAC-SHA256\",\n keyHint: process.env.CASTLE_EPISTEMIC_SEAL_SECRET ? \"CASTLE_EPISTEMIC_SEAL_SECRET\" : \"CASTLE_GATEWAY_TOKEN\",\n issuedAt: Date.now(),\n gateway: { service: \"castle-gateway\", listenPort: PORT }\n };\n sendJson(res, 200, {\n ok: true,\n hash,\n signature,\n attestation,\n canonicalBytes: Buffer.byteLength(canonical, \"utf8\")\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"seal_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicLogsBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`epistemic_logs:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 120) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const persisted = await persistEpistemicLedgerBatch(auth.uid, entries);\n recordGenesisEpistemicLedgerPersisted(persisted.persisted);\n // Internal cognition layer: forecast stays gateway/ledger-only (no client exposure).\n try {\n await persistEpistemicForecastBatch(auth.uid, persisted.normalized || []);\n } catch {\n /* forecast failures must not break primary ledger ingest */\n }\n sendJson(res, 200, {\n ok: true,\n persisted: persisted.persisted,\n mode: persisted.mode,\n latest: persisted.normalized?.[persisted.normalized.length - 1] || null\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"epistemic_log_ingest_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.authorityLedgerBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`authority_ledger:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 80) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const witnessed = persistAuthorityLedgerWitnessBatchV1(auth.uid, entries, EPISTEMIC_SEAL_SECRET);\n if (witnessed.witnessed > 0) {\n recordGenesisEpistemicLedgerPersisted(witnessed.witnessed);\n const clientSeal = witnessed.lastWitness?.clientSealHash;\n if (clientSeal) recordGenesisEpistemicSealIssued(clientSeal);\n }\n sendJson(res, 200, {\n ok: true,\n witnessed: witnessed.witnessed,\n quarantined: witnessed.quarantined,\n chainHeight: witnessed.chainHeight,\n chainHead: witnessed.chainHead,\n lastWitness: witnessed.lastWitness,\n results: witnessed.results\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"authority_ledger_witness_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.authorityLedgerReplay) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const replayUrl = new URL(String(req.url || \"\"), \"http://localhost\");\n const epochId = String(replayUrl.searchParams.get(\"epochId\") || \"\").trim() || undefined;\n const snapshot = getAuthorityLedgerWitnessSnapshotV1(auth.uid, epochId);\n const replay = replayAuthorityLedgerWitnessChainV1(auth.uid, epochId);\n sendJson(res, 200, {\n ok: true,\n snapshot,\n replay,\n schema: \"castle.rhizoh.authority_ledger_witness.v1.replay_surface\"\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"authority_ledger_replay_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.authorityEpochMerge) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`authority_epoch_merge:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const merged = assimilateAuthorityEpochMergeV1(auth.uid, body);\n if (!merged.ok) {\n sendJson(res, 400, merged);\n return;\n }\n sendJson(res, 200, {\n ok: true,\n mergeEvent: merged.mergeEvent,\n recentMerges: merged.recentMerges\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"authority_epoch_merge_failed\") });\n }\n return;\n }\n\n sendJson(res, 404, { ok: false, error: \"not_found\" });\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\nspiralWssForGenesis = wss;\nconst orchestrator = createOrchestrator();\nconst clientStats = new Map();\nlet broadcasterClientId = null;\nconst spiralState = {\n rooms: [{ id: \"room-default\", name: \"Spiral Default Arena\" }],\n activeByClient: new Map(),\n characters: new Map() // id -> {id,name,role,roomId,source}\n};\n\n/** Castle social rooms — keyed by `castleRoomKey` from {@link WS_MESSAGE.CASTLE_SOCIAL_PULSE}. */\nconst castleSocialRoomState = new Map();\nconst CASTLE_SOCIAL_MEMBER_TTL_MS = 90_000;\nconst CASTLE_SOCIAL_MAX_PULSE_BYTES = 8192;\nconst RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES = 5 * 1024 * 1024;\nconst RHIZOH_VOICE_LIVE_MAX_CHUNKS = 96;\n\n/**\n * Voice Engine v3 live media lane.\n * Client streams MediaRecorder chunks over the existing gateway WS; STOP reuses the\n * gateway ASR runner so orchestration remains the single authority downstream.\n */\nconst rhizohVoiceLiveSessions = new WeakMap();\n\nfunction appendRhizohVoiceLiveEventV0(session, type, payloadRefSeed = \"\") {\n if (!session) return null;\n session.eventSeq = (Number(session.eventSeq) || 0) + 1;\n const event = createRhizohImmutableEventV0({\n type,\n sessionId: session.sessionId,\n traceId: session.traceId,\n actorSeed: session.sessionId || session.traceId,\n eventSeq: session.eventSeq,\n payloadRef: createRhizohPayloadRefV0(payloadRefSeed || `${session.sessionId}:${type}:${session.eventSeq}`)\n });\n session.events = [...(session.events || []), event].slice(-64);\n return event;\n}\n\nfunction sendRhizohVoiceLiveEnvelopeV0(socket, type, payload) {\n try {\n if (socket?.readyState === 1) socket.send(JSON.stringify(createEnvelope(type, payload)));\n } catch {\n /* noop */\n }\n}\n\nfunction decodeRhizohVoiceLiveChunkV0(payload = {}) {\n const b64 = String(payload.audioBase64 || \"\").trim();\n if (!b64) return { ok: false, error: \"audio_base64_required\" };\n try {\n const buf = Buffer.from(b64, \"base64\");\n if (!buf.length) return { ok: false, error: \"audio_decode_empty\" };\n return { ok: true, buffer: buf };\n } catch {\n return { ok: false, error: \"audio_base64_invalid\" };\n }\n}\n\nfunction handleRhizohVoiceLiveStartV0(socket, payload = {}) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const sessionId = String(p.sessionId || `live_${Date.now().toString(36)}`).slice(0, 96);\n rhizohVoiceLiveSessions.set(socket, {\n sessionId,\n traceId: String(p.traceId || \"\").slice(0, 128),\n mimeType: String(p.mimeType || \"audio/webm\").slice(0, 80),\n languageCode: String(p.languageCode || \"tr-TR\").slice(0, 32),\n path: String(p.path || \"both\").slice(0, 24),\n chunks: [],\n chunkIndexes: [],\n totalBytes: 0,\n nextChunkIndex: 1,\n eventSeq: 0,\n events: [],\n startedAtMs: Date.now()\n });\n appendRhizohVoiceLiveEventV0(rhizohVoiceLiveSessions.get(socket), \"VOICE_LIVE_SESSION_STARTED\", `${sessionId}:live_session`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_START, {\n ok: true,\n sessionId,\n schema: \"castle.rhizoh.voice_live.gateway_lane.v0\"\n });\n}\n\nfunction handleRhizohVoiceLiveChunkV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const sequence = validateRhizohVoiceLiveChunkPayloadV0(payload, {\n expectedChunkIndex: session.nextChunkIndex\n });\n if (!sequence.ok) {\n rhizohVoiceLiveSessions.delete(socket);\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:chunk_reject`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: sequence.error,\n expectedChunkIndex: sequence.expectedChunkIndex,\n chunkIndex: sequence.chunkIndex\n });\n return;\n }\n const decoded = decodeRhizohVoiceLiveChunkV0(payload);\n if (!decoded.ok) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: decoded.error\n });\n return;\n }\n if (\n session.chunks.length >= RHIZOH_VOICE_LIVE_MAX_CHUNKS ||\n session.totalBytes + decoded.buffer.length > RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES\n ) {\n rhizohVoiceLiveSessions.delete(socket);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_audio_too_large\"\n });\n return;\n }\n session.chunks.push(decoded.buffer);\n session.chunkIndexes.push(sequence.chunkIndex);\n session.totalBytes += decoded.buffer.length;\n session.nextChunkIndex += 1;\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_CHUNK_ACCEPTED\", `${session.sessionId}:chunk:${sequence.chunkIndex}`);\n}\n\nfunction handleRhizohVoiceLiveStopV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n rhizohVoiceLiveSessions.delete(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const lastChunkIndex = Number(payload?.lastChunkIndex);\n if (Number.isInteger(lastChunkIndex) && lastChunkIndex !== session.chunks.length) {\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:stop_mismatch`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_chunk_count_mismatch\",\n expectedChunkCount: lastChunkIndex,\n receivedChunkCount: session.chunks.length\n });\n return;\n }\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_STOP_RECEIVED\", `${session.sessionId}:stop:${session.chunks.length}`);\n const audio = Buffer.concat(session.chunks);\n if (!audio.length) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"audio_empty\"\n });\n return;\n }\n runRhizohVoiceTranscribeV3({\n path: String(payload?.path || session.path || \"both\"),\n audioBase64: audio.toString(\"base64\"),\n mimeType: String(payload?.mimeType || session.mimeType || \"audio/webm\"),\n languageCode: String(payload?.languageCode || session.languageCode || \"tr-TR\"),\n traceId: String(payload?.traceId || session.traceId || \"\"),\n sessionId: session.sessionId,\n transport: \"gateway_ws_live\"\n })\n .then((result) => {\n appendRhizohVoiceLiveEventV0(session, result?.ok ? \"VOICE_TRANSCRIBE_FINAL\" : \"VOICE_TRANSCRIBE_FAILED\", `${session.sessionId}:final`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_FINAL, {\n ...result,\n sessionId: session.sessionId,\n transportPath: \"gateway_ws_live\",\n bytes: audio.length,\n chunkCount: session.chunks.length,\n chunkSequence: Object.freeze({\n ok: true,\n firstChunkIndex: session.chunkIndexes[0] || null,\n lastChunkIndex: session.chunkIndexes[session.chunkIndexes.length - 1] || null,\n receivedChunkCount: session.chunks.length\n }),\n eventTrace: Object.freeze((session.events || []).slice(-16)),\n recordedMs: Math.max(0, Date.now() - session.startedAtMs)\n });\n })\n .catch((e) => {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_transcribe_failed\",\n detail: String(e?.message || e)\n });\n });\n}\n\nfunction removeCastleSocialClientFromAllRooms(clientId) {\n const cid = String(clientId || \"\");\n if (!cid) return;\n for (const room of castleSocialRoomState.values()) {\n room.members.delete(cid);\n }\n}\n\nfunction buildCastleSocialRoomPayload(roomKey) {\n const room = castleSocialRoomState.get(roomKey);\n if (!room) return null;\n const now = Date.now();\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const roster = [...room.members.values()].map((m) => ({\n userId: m.userId,\n lastMs: m.lastMs,\n ...m.sp\n }));\n const peerFeeds =\n room.walPeers && room.walPeers.size\n ? [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }))\n : [];\n return { castleRoomKey: roomKey, seq: room.seq, roster, peerFeeds, ts: now };\n}\n\nconst CASTLE_WAL_PEER_MAX_FEED_BYTES = 48 * 1024;\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {{ ok: boolean, code?: string, reason?: string }} gate\n */\nfunction rejectWalPeerIngressV0(socket, gate) {\n const code = gate.code || \"wal_peer_rejected\";\n recordWalPeerRejectV0(code);\n recordSubstrateOtelEventV0(\"wal_peer_reject\", { code, reason: String(gate.reason || \"\").slice(0, 120) });\n socket.send(\n JSON.stringify(\n createEnvelope(WS_MESSAGE.ERROR, {\n error: gate.reason || \"WAL peer ingress rejected.\",\n code\n })\n )\n );\n}\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {Record<string, unknown> | null | undefined} walPeerFeed\n * @returns {boolean}\n */\nfunction gateWalPeerIngressV0(socket, walPeerFeed) {\n const authGate = assertWalPeerSocketAuthorityV0(socket);\n if (!authGate.ok) {\n rejectWalPeerIngressV0(socket, authGate);\n return false;\n }\n const sigGate = validateWalPeerFeedSignatureV0(\n walPeerFeed && typeof walPeerFeed === \"object\" ? walPeerFeed : {}\n );\n if (!sigGate.ok) {\n rejectWalPeerIngressV0(socket, sigGate);\n return false;\n }\n return true;\n}\n\nfunction handleCastleWalPeerFeed(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_WAL_PEER_MAX_FEED_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_WAL_PEER_FEED too large.\" })));\n return;\n }\n const walPeerFeed = p.walPeerFeed && typeof p.walPeerFeed === \"object\" ? p.walPeerFeed : {};\n if (!gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const castleRoomKey = String(p.castleRoomKey || \"wal:convergence\").slice(0, 64);\n const castleId = String(p.castleId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId,\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: room.members.get(socket.clientId)?.sp ?? {}\n });\n const peerFeeds = [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }));\n const body = {\n castleRoomKey,\n seq: room.seq,\n peerFeeds,\n ts: now\n };\n const env = createEnvelope(WS_MESSAGE.CASTLE_WAL_PEER_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys(), ...room.walPeers.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction handleCastleSocialPulse(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_SOCIAL_MAX_PULSE_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_SOCIAL_PULSE too large.\" })));\n return;\n }\n const castleRoomKey = String(p.castleRoomKey || \"default\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n const sp = p.socialPulse && typeof p.socialPulse === \"object\" ? p.socialPulse : {};\n const walPeerFeed =\n p.walPeerFeed && typeof p.walPeerFeed === \"object\"\n ? p.walPeerFeed\n : sp.walPeerFeed && typeof sp.walPeerFeed === \"object\"\n ? sp.walPeerFeed\n : null;\n if (walPeerFeed && !gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const spNext = walPeerFeed ? { ...sp, walPeerFeed } : { ...sp };\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: spNext\n });\n if (walPeerFeed) {\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId: String(p.castleId || userId).slice(0, 64),\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n }\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const body = buildCastleSocialRoomPayload(castleRoomKey);\n if (!body) return;\n const env = createEnvelope(WS_MESSAGE.CASTLE_SOCIAL_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcast(msgObj) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcastState() {\n broadcast(\n createEnvelope(WS_MESSAGE.BROADCAST_STATE, {\n broadcasterClientId,\n viewers: [...wss.clients].filter((c) => c.readyState === 1).length\n })\n );\n}\n\nfunction currentSpiralStatePayload() {\n return {\n rooms: spiralState.rooms,\n characters: [...spiralState.characters.values()]\n };\n}\n\nfunction broadcastSpiralState() {\n broadcast(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload()));\n}\n\nfunction ensureRoom(roomId, fallbackName = null) {\n if (!roomId) return spiralState.rooms[0].id;\n const exists = spiralState.rooms.find((r) => r.id === roomId);\n if (exists) return exists.id;\n const created = { id: roomId, name: fallbackName || `Spiral-${roomId.slice(-4)}` };\n spiralState.rooms.push(created);\n return created.id;\n}\n\nfunction syncSpiralCharactersWithWorld(snapshot) {\n const agents = snapshot?.agents || [];\n for (const a of agents) {\n if (!spiralState.characters.has(a.id)) {\n spiralState.characters.set(a.id, {\n id: a.id,\n name: a.id,\n role: \"agent\",\n roomId: spiralState.rooms[0].id,\n source: \"world\"\n });\n }\n }\n}\n\nfunction broadcastExcept(msgObj, exceptSocket) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client !== exceptSocket && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction validateInputFrame(payload, socket) {\n const stats = clientStats.get(socket.clientId) || { lastFrame: 0, windowStart: Date.now(), msgCount: 0 };\n const now = Date.now();\n if (now - stats.windowStart > 5000) {\n stats.windowStart = now;\n stats.msgCount = 0;\n }\n stats.msgCount += 1;\n clientStats.set(socket.clientId, stats);\n if (stats.msgCount > 120) return \"Rate limit exceeded.\";\n\n if (!payload || !Number.isInteger(payload.frame)) return \"frame must be integer.\";\n if (payload.frame <= stats.lastFrame) return \"frame must be monotonic.\";\n const commands = Array.isArray(payload.commands) ? payload.commands : [];\n if (commands.length > 8) return \"Too many commands in one frame.\";\n for (const cmd of commands) {\n if (!cmd || typeof cmd.command !== \"string\") return \"Invalid command format.\";\n if (\n cmd.command !== \"SPAWN_AGENT\" &&\n cmd.command !== \"LIST_CASTLES\" &&\n cmd.command !== \"SPAWN_ENTITY\" &&\n cmd.command !== \"CREATE_CASTLE\"\n ) return \"Unsupported command.\";\n }\n stats.lastFrame = payload.frame;\n clientStats.set(socket.clientId, stats);\n return null;\n}\n\nfunction validateSignalPayload(payload) {\n if (!payload || typeof payload !== \"object\") return \"Invalid signaling payload.\";\n const signalType = payload.signalType;\n if (Object.values(CASTLE_NETWORK_SIGNAL_V0).includes(signalType)) {\n return null;\n }\n if (![\"OFFER\", \"ANSWER\", \"ICE\"].includes(signalType)) return \"Invalid signal type.\";\n if (typeof payload.to !== \"string\" || payload.to.length < 3) return \"Invalid signal destination.\";\n if (signalType !== \"ICE\" && JSON.stringify(payload.sdp || {}).length > 16 * 1024) return \"SDP too large.\";\n if (signalType === \"ICE\" && JSON.stringify(payload.candidate || {}).length > 6 * 1024) return \"ICE too large.\";\n return null;\n}\n\nfunction sanitizeRawMessage(raw) {\n if (!raw) return null;\n const text = raw.toString();\n if (Buffer.byteLength(text, \"utf8\") > MAX_MESSAGE_BYTES) return null;\n return text;\n}\n\nwss.on(\"connection\", async (socket, req) => {\n const origin = req?.headers?.origin || \"\";\n if (!httpCors.isWebSocketOriginAllowed(origin)) {\n socket.close(1008, \"Origin not allowed\");\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const path = req?.url || \"\";\n const query = new URL(`ws://localhost${path}`).searchParams;\n const token = query.get(\"token\");\n if (!token || token !== REQUIRED_GATEWAY_TOKEN) {\n socket.close(1008, \"Invalid gateway token\");\n return;\n }\n }\n if (REQUIRE_AUTH) {\n const result = await verifyClientToken(req);\n if (!result.ok) {\n const isLocal = origin.includes(\"localhost\") || origin.includes(\"127.0.0.1\") || !origin;\n if (!(ALLOW_DEV_ANON && isLocal)) {\n socket.close(1008, result.reason);\n return;\n }\n socket.auth = { ok: true, kind: \"dev-anon\", user: { uid: \"dev-anon\" } };\n } else {\n socket.auth = result;\n }\n }\n\n socket.clientId = `c-${Math.random().toString(36).slice(2, 8)}`;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.HELLO, { message: \"Castle Gateway online.\", clientId: socket.clientId })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.WORLD_SNAPSHOT, orchestrator.worldSnapshot())));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.BROADCAST_STATE, { broadcasterClientId, viewers: [...wss.clients].filter((c) => c.readyState === 1).length })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload())));\n broadcastExcept(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }), socket);\n\n socket.on(\"message\", (raw) => {\n const safeRaw = sanitizeRawMessage(raw);\n if (!safeRaw) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Message too large or invalid.\" })));\n return;\n }\n const parsed = safeJsonParse(safeRaw);\n if (!parsed?.type) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid message.\" })));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND_TEXT) {\n const canonical = parseTextToCommand(parsed.payload?.text);\n if (!canonical) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Unknown command text.\" })));\n return;\n }\n const result = orchestrator.applyCommand(canonical);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.MATCH_SESSION_JOIN) {\n const joined = handleMatchSessionJoinV0(socket, parsed, wss);\n const sessionId = String(parsed.sessionId || parsed.payload?.sessionId || \"\").trim();\n if (joined?.ok && sessionId) {\n sendMatchSessionSnapshotOnJoinV0(socket, sessionId);\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.MATCH_CASTLE_INVITE) {\n handleMatchCastleInviteV0(socket, parsed, wss);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.MATCH_MOVE) {\n handleMatchMoveAuthorityV0(socket, parsed, wss);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND) {\n const result = orchestrator.applyCommand(parsed.payload);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n }\n\n if (parsed.type === WS_MESSAGE.INPUT_FRAME) {\n const err = validateInputFrame(parsed.payload, socket);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const frame = parsed.payload.frame;\n const commands = Array.isArray(parsed.payload?.commands) ? parsed.payload.commands : [];\n orchestrator.queueInputFrame(socket.clientId, frame, commands);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, queued: true, frame })));\n }\n\n if (parsed.type === WS_MESSAGE.SIGNAL) {\n const payload = parsed.payload || {};\n if (Object.values(CASTLE_NETWORK_SIGNAL_V0).includes(payload.signalType)) {\n const netErr = validateCastleNetworkSignalPayloadV0(socket, payload);\n if (netErr) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: netErr })));\n return;\n }\n handleCastleNetworkSignalV0(socket, payload, wss);\n return;\n }\n const err = validateSignalPayload(payload);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const signalType = payload.signalType;\n const to = payload.to;\n for (const client of wss.clients) {\n if (client.clientId === to && client.readyState === 1) {\n client.send(JSON.stringify(createEnvelope(WS_MESSAGE.SIGNAL, { ...payload, from: socket.clientId })));\n break;\n }\n }\n }\n\n if (parsed.type === WS_MESSAGE.DRONE_TELEMETRY) {\n const payload = parsed.payload || {};\n const json = JSON.stringify(payload);\n if (Buffer.byteLength(json, \"utf8\") > 48 * 1024) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"DRONE_TELEMETRY payload too large.\" })));\n return;\n }\n broadcastExcept(createEnvelope(WS_MESSAGE.DRONE_TELEMETRY, { ...payload, relayFrom: socket.clientId }), socket);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_START) {\n handleRhizohVoiceLiveStartV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_CHUNK) {\n handleRhizohVoiceLiveChunkV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_STOP) {\n handleRhizohVoiceLiveStopV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_SOCIAL_PULSE) {\n handleCastleSocialPulse(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_WAL_PEER_FEED) {\n handleCastleWalPeerFeed(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.OPEN_DATA_QUERY) {\n queryOpenData(parsed.payload)\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: true, result })));\n })\n .catch((e) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: false, error: e.message })));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_QUERY) {\n runRhizohBrainV2({\n ...(parsed.payload || {}),\n uid: socket?.auth?.user?.uid || \"unknown\"\n })\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, result)));\n })\n .catch(async () => {\n const fallback = await runRhizohBrain(parsed.payload || {}).catch(() => ({\n ok: false,\n reply: \"Rhizoh temporary disturbance. Habitat remains stable.\"\n }));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, fallback)));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.BROADCAST_REGISTER) {\n const role = String(parsed.payload?.role || \"\");\n if (role === \"GENESIS_BROADCAST_AGENT\") {\n broadcasterClientId = socket.clientId;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, registered: role })));\n broadcastState();\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.STUDIO_CUE) {\n // Only registered broadcaster can push global cues.\n if (!broadcasterClientId || socket.clientId !== broadcasterClientId) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Only broadcaster agent can emit STUDIO_CUE.\" })));\n return;\n }\n const cue = parsed.payload || {};\n if (typeof cue !== \"object\") {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid STUDIO_CUE payload.\" })));\n return;\n }\n broadcast(createEnvelope(WS_MESSAGE.STUDIO_CUE, cue));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_JOIN_ROOM) {\n const requested = String(parsed.payload?.roomId || \"\").trim();\n const requestedName = String(parsed.payload?.roomName || \"\").trim();\n const roomId = ensureRoom(requested || `room-${Math.random().toString(36).slice(2, 7)}`, requestedName || null);\n spiralState.activeByClient.set(socket.clientId, roomId);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, roomId })));\n broadcastSpiralState();\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_CREATE_CHARACTER) {\n const name = String(parsed.payload?.name || \"\").trim().slice(0, 32) || `AI-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;\n const role = String(parsed.payload?.role || \"agent\").trim().slice(0, 24) || \"agent\";\n const roomId = ensureRoom(\n String(parsed.payload?.roomId || spiralState.activeByClient.get(socket.clientId) || spiralState.rooms[0].id),\n null\n );\n\n // Character creation always maps to an authoritative world agent.\n const result = orchestrator.applyCommand({\n command: COMMAND.SPAWN_AGENT,\n params: { targetCastleId: \"castle-01\" }\n });\n if (!result?.ok || !result?.data?.id) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Character creation failed.\" })));\n return;\n }\n const id = result.data.id;\n spiralState.characters.set(id, { id, name, role, roomId, source: \"spawned\" });\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, characterId: id, roomId })));\n broadcastSpiralState();\n return;\n }\n });\n\n socket.on(\"close\", () => {\n if (socket.clientId === broadcasterClientId) broadcasterClientId = null;\n leaveMatchBroadcastRoomV0(socket);\n clientStats.delete(socket.clientId);\n rhizohVoiceLiveSessions.delete(socket);\n spiralState.activeByClient.delete(socket.clientId);\n removeCastleSocialClientFromAllRooms(socket.clientId);\n removeCastleNetworkClientV0(socket.clientId, wss);\n broadcast(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }));\n broadcastState();\n });\n});\n\nsetInterval(() => {\n void (async () => {\n try {\n const { frame, snapshot, delta, packedDelta } = orchestrator.tick(1 / 20);\n syncSpiralCharactersWithWorld(snapshot);\n broadcast(createEnvelope(WS_MESSAGE.NETWORK_TICK, { frame }));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_TICK, snapshot));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA, delta));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA_PACKED, packedDelta));\n broadcastSpiralState();\n await runAcademyEventTick();\n const executed = await tickAutomations(Date.now());\n for (const a of executed) {\n await addTranscript(a.uid, {\n source: \"planner\",\n eventType: \"automation-executed\",\n text: `Automation executed: ${a.title} (${a.action})`,\n roomId: \"studio-main\",\n meta: { automationId: a.id, action: a.action, deviceId: a.deviceId || \"\" }\n });\n }\n } catch (e) {\n console.error(\"[GATEWAY] world tick / automations loop error:\", String(e?.message || e));\n }\n })();\n}, 50);\n\n(async () => {\n await rhizohRuntime.telemetry.initOpenTelemetry();\n if (String(process.env.CASTLE_GENESIS_DISK_PERSIST ?? \"\").trim() === \"\") {\n process.env.CASTLE_GENESIS_DISK_PERSIST = \"1\";\n }\n const genesisDiskBoot = await hydrateGenesisContinuityPersistenceBootV0();\n if (!genesisDiskBoot.ok) {\n console.warn(\"[GATEWAY] genesis disk hydrate failed; continuing with in-memory genesis continuity\");\n }\n httpServer.listen(PORT, () => {\n startGenesisCanonicalClock();\n installGenesisCheckpointSurfaceGetter(buildGenesisRuntimeSurfacePayloadLive);\n setGenesisContinuityAfterPublishHook((seq) => {\n noteGenesisCheckpointSeqCommitted(seq);\n noteGenesisContinuitySeqAcceptedV0(seq);\n });\n startGenesisContinuityInfraSampler(buildGenesisRuntimeSurfacePayloadLive, 2000);\n logProductionObservatorySurfaceGuardsV0();\n logGatewaySubstrateAuthorityGuardsV0();\n if (isLlmWorkerEnabledV0()) {\n try {\n const workerBoot = initLlmWorkerAtBootV0();\n console.log(\"[GATEWAY] llm worker boot:\", workerBoot);\n } catch (workerErr) {\n console.error(\"[GATEWAY] llm worker boot failed:\", workerErr);\n }\n }\n console.log(`[GATEWAY] ws/http://localhost:${PORT}`);\n console.log(\n `[GATEWAY] genesis continuity: GET ${rhizohRuntime.routes.genesisRuntime} | POST ${rhizohRuntime.routes.genesisIngress} | SSE ${rhizohRuntime.routes.genesisStream} | replay ${rhizohRuntime.routes.genesisReplay} | diff ${rhizohRuntime.routes.genesisReplayDiff} | equiv ${rhizohRuntime.routes.genesisReplayEquivalence} | analytics ${rhizohRuntime.routes.genesisReplayAnalytics} | evolution ${rhizohRuntime.routes.genesisReplayEvolution}`\n );\n if (String(process.env.CASTLE_GENESIS_OBSERVATORY_LIVE_V01 ?? \"\").trim() === \"1\") {\n console.log(\"GENESIS OBSERVATORY LIVE v0.1 — READ ONLY PHASE STARTED\");\n }\n });\n})().catch((e) => {\n console.error(\"[GATEWAY] fatal boot (before or during listen):\", e);\n process.exit(1);\n});\n"}, "4ff0eb6773f43fef8ef90bab4864c0dfb758bdaa": {"t": 1785754120.4672804, "n": 177386, "h": "f633be7b80949663", "c": "import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport dotenv from \"dotenv\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconsole.log(\"🔥 [GENESIS_BOOT] entry file:\", __filename);\nconsole.log(\"🔥 [GENESIS_BOOT] node env:\", process.env.NODE_ENV);\nconsole.log(\"🔥 [GENESIS_BOOT] pid:\", process.pid);\nconsole.log(\"🧬 [GENESIS_BOOT] cwd:\", process.cwd());\nconsole.log(\"🧬 [GENESIS_BOOT] import.meta.url:\", import.meta.url);\ndotenv.config({ path: path.join(__dirname, \"..\", \".env\") });\ndotenv.config({ path: path.join(__dirname, \"..\", \".env.local\"), override: true });\n\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport { createServer } from \"node:http\";\nimport { createOrchestrator } from \"@castle/orchestrator\";\nimport { parseTextToCommand } from \"@castle/command-dsl\";\nimport {\n WS_MESSAGE,\n COMMAND,\n createEnvelope,\n createRhizohImmutableEventV0,\n createRhizohPayloadRefV0,\n safeJsonParse,\n validateRhizohVoiceLiveChunkPayloadV0\n} from \"@castle/protocol\";\nimport { queryOpenData } from \"./openData.js\";\nimport { verifyClientToken } from \"./auth.js\";\nimport { handleMatchMoveAuthorityV0, leaveMatchBroadcastRoomV0 } from \"./rhizoh/matchMoveAuthorityV0.js\";\nimport { handleMatchSessionJoinV0, handleMatchCastleInviteV0 } from \"./rhizoh/matchBroadcastRoomV0.js\";\nimport { queryRhizohLlm } from \"./rhizohLlmGateway.js\";\nimport { rhizohGatewayTurn } from \"./rhizohGatewayTurn.js\";\nimport {\n enqueueLlmWorkerTaskAsyncV0,\n getLatestLlmWorkerTaskIdV0,\n getLlmWorkerDebugSnapshotV0,\n getLlmWorkerTaskSnapshotV0,\n initLlmWorkerAtBootV0,\n isLlmWorkerAsyncEnabledV0,\n isLlmWorkerEnabledV0,\n runLlmWorkerTaskV0\n} from \"./llmWorkerRuntime.js\";\nimport {\n RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n runRhizohVoiceTranscribeV3,\n rhizohVoiceTranscribeEnvV3\n} from \"./rhizohVoiceTranscribeV3.js\";\nimport { createHttpCorsPolicy, normalizeHttpOrigin, parseAllowedOriginsFromEnv } from \"./httpCorsPolicyV1.js\";\nimport {\n applyLanguagePropagationToLlmPayloadV1,\n applyLanguagePropagationToVoiceBodyV1,\n buildRhizohLanguagePropagationEchoV1,\n parseRhizohLanguagePropagationV1\n} from \"./rhizohLanguagePropagationV1.js\";\nimport { trimRhizohLlmGatewayPayloadV1 } from \"./rhizohLlmPayloadTrimV1.js\";\nimport {\n meshAppendDelta,\n meshContinuityAggregate,\n meshJoin,\n meshLeave,\n meshReplayFromNodeId,\n meshReplayFromSeq,\n meshSnapshot,\n meshSubscribeSse\n} from \"./presenceRoomMesh.js\";\nimport { runRhizohBrainV2 } from \"./rhizohBrainV2.js\";\nimport {\n listConnections,\n createConnection,\n updateConnection,\n deleteConnection,\n setDefaultConnection,\n resolveConnection\n} from \"./llmConnectionsStore.js\";\nimport { getCloudSyncSnapshotV0, mergeCloudSyncSnapshotV0 } from \"./castleCloudSyncStore.js\";\nimport {\n CASTLE_NETWORK_SIGNAL_V0,\n handleCastleNetworkSignalV0,\n listCastleNetworkPresenceV0,\n removeCastleNetworkClientV0,\n validateCastleNetworkSignalPayloadV0\n} from \"./castleNetworkRelayV0.js\";\nimport { checkHttpRateLimit, getHttpClientIp } from \"./castleHttpRateLimit.js\";\nimport {\n submitAbuseReportV0,\n listModerationQueueV0,\n softBlockUserV0,\n removeSoftBlockV0,\n listSoftBlockedUsersV0\n} from \"./ops/moderationMvpV0.js\";\nimport { listRecentAgentSnapshotsV0 } from \"./ops/agentObservabilityV0.js\";\nimport { readAgentContainmentConfigV0 } from \"./ops/agentContainmentV0.js\";\nimport { readCostContainmentConfigV0, getCostContainmentStatsV0 } from \"./ops/costContainmentV0.js\";\nimport { getGlobalCostLedgerSnapshotV0, getGclBackendStatusV0 } from \"./ops/globalCostLedgerV0.js\";\nimport { evaluateEconomicStrategyL3V0 } from \"./ops/economicStrategyEngineL3V0.js\";\nimport { getPhasedRolloutStatsV0, readPhasedRolloutTierV0 } from \"./ops/phasedRolloutV0.js\";\nimport { buildUnifiedStateNarrativeV0 } from \"./ops/unifiedStateNarrativeV0.js\";\nimport { resolveHardeningTenantIdV0 } from \"./ops/narrativeTenantIsolationV0.js\";\nimport {\n classifyStressResponseV0,\n verifyStressTaxonomyCoverageV0,\n verifyStressConflictResolutionV0\n} from \"./ops/stressResponseTaxonomyV0.js\";\nimport { verifyResolutionStabilityEnvelopeV0 } from \"./ops/resolutionStabilityEnvelopeV0.js\";\nimport { logLlmAccess } from \"./castleLlmAudit.js\";\nimport { appendMemory, listMemories, getMemoryContext, getPersonaGoalMemory, setPersonaGoalMemory, autoCompactMemories } from \"./memoryStore.js\";\nimport { getFirebasePersistence } from \"./firebasePersistence.js\";\nimport { registerAgentIdentity, listAgentIdentities, getAgentIdentity, updateAgentIdentity } from \"./agentIdentityStore.js\";\nimport { queueAcademyEvent, listAcademyEvents, resolveAcademyEvent, runAcademyEventTick } from \"./academyEventEngine.js\";\nimport { enqueuePdfJob, getPdfJob } from \"./pdfPipeline.js\";\nimport {\n createPublishSession,\n listPublishSessions,\n updatePublishSession,\n addTranscript,\n listTranscripts,\n searchMetadata,\n registerDevice,\n listDevices,\n updateDevice,\n appendTelemetry,\n listTelemetry,\n createAutomation,\n listAutomations,\n tickAutomations,\n enqueueCommand,\n listCommands,\n updateCommandStatus,\n registerSocialChannel,\n listSocialChannels\n} from \"./studioOpsStore.js\";\nimport { metrics as infraMetrics } from \"./infra/metrics.js\";\nimport { renderRhizohPrometheusMetrics, rhizohEnterpriseMetrics } from \"./infra/rhizohEnterpriseMetrics.js\";\nimport { scoreHealth } from \"./infra/healthScore.js\";\nimport { getTraceById, listTracesBySession } from \"./infra/traceRegistry.js\";\nimport { canonicalEpistemicSealString, hashAndSignEpistemicSeal, EPISTEMIC_SEAL_SCHEMA } from \"./epistemicSeal.js\";\nimport { persistEpistemicLedgerBatch } from \"./epistemicLedgerStore.js\";\nimport { persistEpistemicForecastBatch } from \"./epistemicForecastStore.js\";\nimport {\n persistAuthorityLedgerWitnessBatchV1,\n getAuthorityLedgerWitnessSnapshotV1,\n replayAuthorityLedgerWitnessChainV1\n} from \"./authorityLedgerWitnessStoreV1.js\";\nimport { assimilateAuthorityEpochMergeV1 } from \"./authorityEpochMergeStoreV1.js\";\nimport { buildRhizohExternalGroundTruthPayload } from \"./rhizohExternalGroundTruthGateway.js\";\nimport { ingestRhizohExternalLossBatchHttp } from \"./rhizohExternalLossIngestGateway.js\";\nimport {\n ingestRhizohProductOutcomeHttp,\n listRhizohProductOutcomeAggregates,\n verifyRhizohOutcomeHmac,\n verifyRhizohOutcomeSourceToken\n} from \"./rhizohProductOutcomeIngestGateway.js\";\nimport { initRhizoh } from \"./rhizohProductionBootstrap.js\";\nimport { handleAcademicObservatoryExportGetV0 } from \"./rhizoh/academicObservatoryHttpV0.js\";\nimport { handleCouncilAnomalyReasoningPostV0 } from \"./council/rhizohEpistemicCouncilHttpV0.js\";\nimport {\n handleLiveSportsBundleGetV0,\n handleLiveNewsHeadlinesGetV0,\n handleLiveWorldFeedGetV0\n} from \"./rhizoh/liveWorldFeedHttpV0.js\";\nimport {\n buildGenesisRuntimeSurfacePayload,\n recordGenesisEpistemicLedgerPersisted,\n recordGenesisEpistemicSealIssued,\n startGenesisCanonicalClock\n} from \"./genesisRuntimeSurfaceV0.js\";\nimport { registerGenesisSseClient, getGenesisContinuitySeq, setGenesisContinuityAfterPublishHook } from \"./genesisContinuityStreamHubV0.js\";\nimport { computeGenesisReplayFingerprintV0 } from \"./genesisReplayFingerprintV0.js\";\nimport { buildGenesisGatewayCapabilitiesV0 } from \"./genesisGatewayCapabilitiesV0.js\";\nimport {\n installGenesisCheckpointSurfaceGetter,\n noteGenesisCheckpointSeqCommitted,\n getLatestGenesisSignedCheckpoint\n} from \"./genesisContinuityCheckpointV0.js\";\nimport { hydrateGenesisContinuityPersistenceBootV0 } from \"./genesisContinuityHydrateBootV0.js\";\nimport { startGenesisContinuityInfraSampler } from \"./genesisContinuityInfraSamplerV0.js\";\nimport {\n genesisCheckpointQueryBySeqV0,\n genesisCheckpointQueryRangeV0,\n genesisCheckpointQueryLineageV0\n} from \"./genesisCheckpointQueryV0.js\";\nimport {\n genesisContinuityEventArchiveEnabled,\n queryGenesisContinuityEventArchiveV0,\n GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION\n} from \"./genesisContinuityEventArchiveV0.js\";\nimport { resolveGenesisReplayRouterV1, GENESIS_REPLAY_ROUTER_SCHEMA } from \"./genesisReplayRouterV1.js\";\nimport {\n computeGenesisReplayTemporalDiffV1,\n GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA\n} from \"./genesisReplayTemporalDiffV1.js\";\nimport { compareGenesisReplayEquivalenceV1 } from \"./genesisReplayEquivalenceV1.js\";\nimport { computeGenesisReplayAnalyticsV1 } from \"./genesisReplayAnalyticsV1.js\";\nimport { computeGenesisReplayEvolutionV1 } from \"./genesisReplayEvolutionV1.js\";\nimport { handleStripeCheckoutCreate, handleStripeMembershipWebhook } from \"./stripeWebhookMembership.js\";\nimport { buildRealityHealthPayload } from \"./edgeRealityConsistencyV1.js\";\nimport {\n assertWalPeerSocketAuthorityV0,\n buildGatewaySubstrateAuthoritySnapshotV0,\n logGatewaySubstrateAuthorityGuardsV0,\n validateWalPeerFeedSignatureV0\n} from \"./gatewaySubstrateAuthorityV0.js\";\nimport {\n buildSubstrateOperationalSnapshotV0,\n recordWalPeerRejectV0,\n renderSubstratePrometheusMetricsV0\n} from \"./infra/substrateOperationalMetrics.js\";\nimport { ingestClientSubstrateHealthV0 } from \"./rhizohSubstrateHealthIngestV0.js\";\nimport { recordSubstrateOtelEventV0 } from \"./infra/opentelemetrySubstrateV0.js\";\nimport {\n GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n getGenesisWorldObservationIngressStatusV0,\n ingestGenesisWorldObservationV0\n} from \"./genesisContinuityClientIngressV0.js\";\nimport {\n getGenesisContinuitySeqAuditV0,\n noteGenesisContinuitySeqAcceptedV0\n} from \"./genesisContinuitySeqGapV0.js\";\n\n/** Render / Fly / Railway: `PORT` — yoksa `CASTLE_GATEWAY_PORT` — yoksa 8090. */\nconst PORT =\n Number(process.env.PORT) ||\n Number(process.env.CASTLE_GATEWAY_PORT) ||\n 8090;\n/** Spiral / world WebSocket server — set after `wss` init; used by genesis runtime surface. */\nlet spiralWssForGenesis = null;\nconst MAX_MESSAGE_BYTES = Number(process.env.CASTLE_MAX_MESSAGE_BYTES || 32 * 1024);\nconst REQUIRED_GATEWAY_TOKEN = process.env.CASTLE_GATEWAY_TOKEN || \"\";\nconst ALLOWED_ORIGINS = parseAllowedOriginsFromEnv().fromList;\nconst httpCors = createHttpCorsPolicy();\n\n/** @returns {string|null} `Access-Control-Allow-Origin`; null = browser CORS fail. */\nfunction accessControlAllowOriginValue(req) {\n return httpCors.accessControlAllowOriginValue(req);\n}\n\nfunction applyHttpCorsHeaders(req, res) {\n return httpCors.applyHttpCorsHeaders(req, res);\n}\nconst REQUIRE_AUTH = process.env.CASTLE_REQUIRE_AUTH === \"true\";\nconst ALLOW_DEV_ANON = process.env.CASTLE_ALLOW_DEV_ANON !== \"false\";\nconst ALLOW_DEV_HTTP_UID = process.env.CASTLE_ALLOW_DEV_HTTP_UID !== \"false\";\n/** Presence mesh: allow `X-Castle-Guest-Id` when Firebase auth absent (default on). Set `false` to require auth only. */\nconst ALLOW_MESH_GUEST = process.env.CASTLE_ALLOW_MESH_GUEST !== \"false\";\nconst SOCIAL_RETRY_MAX = Math.max(0, Math.min(5, Number(process.env.CASTLE_SOCIAL_RETRY_MAX || 2)));\nconst SOCIAL_RETRY_BASE_MS = Math.max(100, Number(process.env.CASTLE_SOCIAL_RETRY_BASE_MS || 700));\nconst TELEGRAM_BOT_TOKEN = process.env.CASTLE_TELEGRAM_BOT_TOKEN || \"\";\nconst WHATSAPP_TOKEN = process.env.CASTLE_WHATSAPP_TOKEN || \"\";\nconst WHATSAPP_PHONE_NUMBER_ID = process.env.CASTLE_WHATSAPP_PHONE_NUMBER_ID || \"\";\nconst RL_RHIZOH_LLM_PER_MIN = Math.max(5, Number(process.env.CASTLE_RL_RHIZOH_LLM_PER_MIN || 40));\nconst RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN || 30)\n);\nconst RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN || 120));\nconst RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN = Math.max(\n 10,\n Number(process.env.CASTLE_RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN || 60)\n);\nconst RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN = Math.max(8, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN || 48));\nconst RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN = Math.max(4, Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN || 24));\nconst RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN || 18)\n);\nconst RL_EPISTEMIC_SEAL_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_EPISTEMIC_SEAL_PER_MIN || 120));\nconst RL_EPISTEMIC_LOG_PER_MIN = Math.max(20, Number(process.env.CASTLE_RL_EPISTEMIC_LOG_PER_MIN || 240));\nconst EPISTEMIC_SEAL_SECRET = String(\n process.env.CASTLE_EPISTEMIC_SEAL_SECRET || process.env.CASTLE_GATEWAY_TOKEN || \"\"\n).trim();\nconst RL_LLM_CONN_TEST_PER_MIN = Math.max(2, Number(process.env.CASTLE_RL_LLM_CONNECTION_TEST_PER_MIN || 8));\n\n/** One-shot production warnings: CORS surface, auth toggle, dev bypass flags (observatory deploy hygiene). */\nfunction logProductionObservatorySurfaceGuardsV0() {\n if (String(process.env.NODE_ENV || \"\").toLowerCase() !== \"production\") return;\n const tag = \"[GATEWAY_PROD_GUARD]\";\n const primaryCors = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n const corsListEmpty = ALLOWED_ORIGINS.length === 0;\n if (corsListEmpty && !primaryCors) {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty and CASTLE_HTTP_CORS_ORIGIN empty — production still merges RHIZOH_BROWSER_ORIGINS_V1 defaults; set env for preview/staging hosts`\n );\n } else if (corsListEmpty && primaryCors && primaryCors !== \"*\") {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty — only CASTLE_HTTP_CORS_ORIGIN set; add web.app + firebaseapp.com (and previews) to CASTLE_ALLOWED_ORIGINS if clients use alternate hostnames`\n );\n }\n if (primaryCors && /localhost|127\\.0\\.0\\.1/i.test(primaryCors)) {\n console.warn(`${tag} CORS: CASTLE_HTTP_CORS_ORIGIN is localhost while NODE_ENV=production`);\n }\n if (REQUIRE_AUTH) {\n console.warn(\n `${tag} AUTH: CASTLE_REQUIRE_AUTH=true — clients must send a valid gateway credential (otherwise silent 401 on replay/SSE); EventSource cannot set custom headers unless proxied`\n );\n if (REQUIRED_GATEWAY_TOKEN.length < 16) {\n console.warn(`${tag} AUTH: CASTLE_GATEWAY_TOKEN missing or short while require-auth enabled`);\n }\n }\n if (ALLOW_DEV_ANON) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_ANON is not \"false\" — dev anonymous path may be live in production (set CASTLE_ALLOW_DEV_ANON=false)`\n );\n }\n if (ALLOW_DEV_HTTP_UID) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_HTTP_UID is not \"false\" — X-Castle-Dev-Uid bypass risk (set CASTLE_ALLOW_DEV_HTTP_UID=false)`\n );\n }\n}\n\nconst rhizohRuntime = initRhizoh();\n\n/** İstemci: llmKeySource veya keySource — env | user_connection | auto */\nfunction normalizeClientLlmKeySource(raw) {\n const s = String(raw ?? \"\").trim().toLowerCase();\n if (s === \"env\" || s === \"server\") return \"env\";\n if (s === \"user_connection\" || s === \"connection\" || s === \"user\") return \"user_connection\";\n if (s === \"auto\" || s === \"\") return \"auto\";\n return null;\n}\n\n/**\n * Server-side provider wins for env/auto unless user_connection — client bundle may still send provider=openai.\n * @param {\"env\"|\"user_connection\"|\"auto\"} keyMode\n * @param {{ provider?: string, model?: string, apiKey?: string } | null} conn\n * @param {{ provider?: string, model?: string }} payload\n */\nfunction resolveServerLlmProviderV0(keyMode, conn, payload) {\n const envProvider = String(process.env.CASTLE_LLM_PROVIDER || \"\").trim() || undefined;\n const envModel = String(process.env.CASTLE_LLM_MODEL || \"\").trim() || undefined;\n\n if (keyMode === \"user_connection\") {\n return {\n resolvedProvider: conn?.provider || payload?.provider,\n resolvedModel: conn?.model || payload?.model,\n connApiKey: conn?.apiKey || \"\"\n };\n }\n\n if (keyMode === \"env\") {\n return {\n resolvedProvider: envProvider ?? payload?.provider,\n resolvedModel: envModel ?? payload?.model,\n connApiKey: \"\"\n };\n }\n\n if (conn?.provider && conn?.apiKey) {\n return {\n resolvedProvider: conn.provider,\n resolvedModel: conn.model,\n connApiKey: conn.apiKey\n };\n }\n\n return {\n resolvedProvider: envProvider ?? payload?.provider,\n resolvedModel: envModel ?? payload?.model,\n connApiKey: \"\"\n };\n}\n\nfunction jsonReplacerSafeV0(_key, value) {\n if (typeof value === \"bigint\") return Number(value);\n return value;\n}\n\nfunction sendJson(res, statusCode, payload) {\n res.writeHead(statusCode, { \"Content-Type\": \"application/json; charset=utf-8\" });\n try {\n res.end(JSON.stringify(payload, jsonReplacerSafeV0));\n } catch (error) {\n res.end(\n JSON.stringify({\n ok: false,\n error: \"response_serialize_failed\",\n detail: String(error?.message || error)\n })\n );\n }\n}\n\nfunction logRhizohHealth(stage, detail = {}) {\n try {\n console.info(`[CASTLE_${String(stage || \"unknown\")}]`, detail && typeof detail === \"object\" ? detail : {});\n } catch {\n /* noop */\n }\n}\n\nfunction renderInfraMetricsProm() {\n const lines = [\n `castle_gateway_events_processed_total ${infraMetrics.eventsProcessed}`,\n `castle_gateway_duplicate_reject_total ${infraMetrics.duplicateRejects}`,\n `castle_gateway_enqueue_latency_ms ${infraMetrics.enqueueLatencyMs}`,\n `castle_gateway_queue_depth ${infraMetrics.queueDepth}`,\n `castle_gateway_queue_lag_seconds ${(Number(infraMetrics.queueLag || 0) / 1000).toFixed(3)}`,\n `castle_gateway_errors_total ${infraMetrics.errors}`\n ];\n const buckets = infraMetrics.enqueueLatencyBuckets || {};\n for (const k of Object.keys(buckets)) {\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"${k}\"} ${buckets[k]}`);\n }\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"+Inf\"} ${infraMetrics.eventsProcessed}`);\n if (process.env.CASTLE_SUBSTRATE_METRICS !== \"0\") {\n lines.push(renderSubstratePrometheusMetricsV0().trimEnd());\n }\n return lines.join(\"\\n\");\n}\n\nasync function fetchJsonWithTimeout(url, timeoutMs = 1200) {\n const ctl = new AbortController();\n const tid = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const res = await fetch(url, { signal: ctl.signal });\n if (!res.ok) return null;\n return await res.json();\n } catch {\n return null;\n } finally {\n clearTimeout(tid);\n }\n}\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction getHttpPathname(req) {\n let p = String(req.url || \"\").split(\"?\")[0];\n /* Reverse proxies / clients sometimes append `/` — avoid silent 404 on e.g. `/rhizoh/genesis/stream/`. */\n while (p.length > 1 && p.endsWith(\"/\")) {\n p = p.slice(0, -1);\n }\n return p;\n}\n\nfunction rhizohLlmEnvConfigured() {\n const p = String(process.env.CASTLE_LLM_PROVIDER || \"openai\").toLowerCase();\n const key =\n p === \"anthropic\"\n ? process.env.ANTHROPIC_API_KEY\n : p === \"gemini\"\n ? process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY\n : p === \"xai\"\n ? process.env.XAI_API_KEY\n : p === \"deepseek\"\n ? process.env.DEEPSEEK_API_KEY\n : p === \"mistral\"\n ? process.env.MISTRAL_API_KEY\n : p === \"openrouter\"\n ? process.env.OPENROUTER_API_KEY\n : process.env.OPENAI_API_KEY;\n return !!String(key || \"\").trim();\n}\n\n/**\n * @param {import(\"firebase-admin/firestore\").Firestore | null} db\n * @param {number} timeoutMs\n */\nasync function probeFirestoreReachable(db, timeoutMs = 2000) {\n if (!db) return { ok: false, reason: \"no_db\" };\n const timeout = sleep(timeoutMs).then(() => ({ ok: false, reason: \"timeout\" }));\n const work = db\n .collection(\"_castle_gateway_health\")\n .doc(\"ping\")\n .get()\n .then(() => ({ ok: true, reason: \"reachable\" }))\n .catch((e) => ({ ok: false, reason: String(e?.message || e || \"firestore_error\") }));\n return Promise.race([work, timeout]);\n}\n\nfunction stripHtml(text = \"\") {\n return String(text)\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \" \")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \" \")\n .replace(/<[^>]+>/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction haversineMeters(aLat, aLon, bLat, bLon) {\n const R = 6371000;\n const dLat = ((bLat - aLat) * Math.PI) / 180;\n const dLon = ((bLon - aLon) * Math.PI) / 180;\n const sa = Math.sin(dLat / 2);\n const sb = Math.sin(dLon / 2);\n const x = sa * sa + Math.cos((aLat * Math.PI) / 180) * Math.cos((bLat * Math.PI) / 180) * sb * sb;\n return 2 * R * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));\n}\n\nasync function checkPublishTargetHealth(protocol, target) {\n const p = String(protocol || \"\").toUpperCase();\n const t = String(target || \"\").trim();\n if (!t) return { ok: false, reason: \"target_missing\", shouldReconnect: false };\n if (p === \"RTMP\") {\n const ok = t.startsWith(\"rtmp://\") || t.startsWith(\"rtmps://\");\n return { ok, reason: ok ? \"rtmp_target_format_ok\" : \"invalid_rtmp_target\", shouldReconnect: !ok, reconnectInSec: 15 };\n }\n if (p === \"WHIP\" || p === \"SFU\") {\n try {\n const res = await fetch(t, { method: \"OPTIONS\" });\n const ok = res.status < 500;\n return { ok, reason: `http_${res.status}`, shouldReconnect: !ok, reconnectInSec: 8 };\n } catch {\n return { ok: false, reason: \"network_unreachable\", shouldReconnect: true, reconnectInSec: 8 };\n }\n }\n return { ok: true, reason: \"unchecked_protocol\", shouldReconnect: false };\n}\n\nasync function fetchTextFromUrl(url) {\n const res = await fetch(url, { method: \"GET\" });\n if (!res.ok) throw new Error(`fetch_${res.status}`);\n const ct = String(res.headers.get(\"content-type\") || \"\");\n if (ct.includes(\"application/pdf\")) throw new Error(\"pdf_binary_not_supported_directly\");\n const body = await res.text();\n return stripHtml(body).slice(0, 14000);\n}\n\nfunction buildTaskActionGraph(task = \"\", context = {}) {\n const t = String(task || \"\").toLowerCase();\n if (t.includes(\"dinner\") || t.includes(\"akşam yemeği\") || t.includes(\"aksam yemegi\")) {\n return {\n taskTitle: \"Dinner Prep Planner\",\n nodes: [\n { id: \"n1\", title: \"Kitchen preheat\", action: \"smart-oven.preheat\", params: { temperatureC: 190, mode: \"fan\" } },\n { id: \"n2\", title: \"Lighting comfort\", action: \"smart-light.set_scene\", params: { scene: \"warm_dinner\" } },\n { id: \"n3\", title: \"Fridge inventory check\", action: \"smart-fridge.inventory_scan\", params: {} },\n { id: \"n4\", title: \"Reminder notification\", action: \"smart-watch.notify\", params: { text: \"Dinner prep started.\" } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" },\n { from: \"n3\", to: \"n4\" }\n ]\n };\n }\n return {\n taskTitle: task || \"General Planner\",\n nodes: [\n { id: \"n1\", title: \"Safety check\", action: \"device.safety_check\", params: { context } },\n { id: \"n2\", title: \"Primary action\", action: \"device.execute_task\", params: { task } },\n { id: \"n3\", title: \"Report\", action: \"studio.timeline_note\", params: { task } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" }\n ]\n };\n}\n\nfunction buildEthicsPrograms() {\n return {\n policyVersion: \"v1\",\n tracks: [\n {\n id: \"elderly-care-assist\",\n audience: \"elderly\",\n title: \"Elderly Assistance Track\",\n principles: [\"human-in-the-loop\", \"consent-first\", \"low-friction alerts\"],\n modules: [\"medication-reminder\", \"fall-risk-check\", \"family-notify\", \"daily-companion-brief\"]\n },\n {\n id: \"kids-safe-learning\",\n audience: \"children\",\n title: \"Kids Safe Learning Track\",\n principles: [\"guardian-consent\", \"age-appropriate-language\", \"content-filtering\"],\n modules: [\"study-coach\", \"screen-time-balance\", \"safe-social-share\", \"emergency-contact\"]\n }\n ]\n };\n}\n\nasync function deliverViaTelegram(channel, text) {\n const chatId = String(channel.endpoint || \"\").trim();\n if (!TELEGRAM_BOT_TOKEN) throw new Error(\"telegram_token_missing\");\n if (!chatId) throw new Error(\"telegram_chat_id_missing\");\n const url = `https://api.telegram.org/bot${encodeURIComponent(TELEGRAM_BOT_TOKEN)}/sendMessage`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ chat_id: chatId, text })\n });\n if (!res.ok) throw new Error(`telegram_http_${res.status}`);\n return { provider: \"telegram\", status: res.status };\n}\n\nasync function deliverViaWhatsapp(channel, text) {\n const to = String(channel.endpoint || \"\").trim();\n if (!WHATSAPP_TOKEN) throw new Error(\"whatsapp_token_missing\");\n if (!WHATSAPP_PHONE_NUMBER_ID) throw new Error(\"whatsapp_phone_number_id_missing\");\n if (!to) throw new Error(\"whatsapp_to_missing\");\n const url = `https://graph.facebook.com/v22.0/${encodeURIComponent(WHATSAPP_PHONE_NUMBER_ID)}/messages`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${WHATSAPP_TOKEN}` },\n body: JSON.stringify({ messaging_product: \"whatsapp\", to, type: \"text\", text: { body: text } })\n });\n if (!res.ok) throw new Error(`whatsapp_http_${res.status}`);\n return { provider: \"whatsapp\", status: res.status };\n}\n\nasync function deliverViaWebhook(channel, text) {\n const endpoint = String(channel.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"webhook_endpoint_missing\");\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text, platform: channel.platform || \"webhook\", ts: Date.now() })\n });\n if (!res.ok) throw new Error(`webhook_http_${res.status}`);\n return { provider: \"webhook\", status: res.status };\n}\n\nasync function deliverSocialMessage(channel, text) {\n const platform = String(channel.platform || \"webhook\").toLowerCase();\n if (platform === \"telegram\") return deliverViaTelegram(channel, text);\n if (platform === \"whatsapp\") return deliverViaWhatsapp(channel, text);\n return deliverViaWebhook(channel, text);\n}\n\nasync function deliverSocialWithRetry(channel, text) {\n let attempt = 0;\n let lastError = null;\n while (attempt <= SOCIAL_RETRY_MAX) {\n try {\n const result = await deliverSocialMessage(channel, text);\n return { ok: true, attempt: attempt + 1, ...result };\n } catch (error) {\n lastError = error;\n if (attempt >= SOCIAL_RETRY_MAX) break;\n const jitter = Math.floor(Math.random() * 90);\n await sleep(SOCIAL_RETRY_BASE_MS * Math.pow(2, attempt) + jitter);\n }\n attempt += 1;\n }\n return { ok: false, attempt: attempt + 1, error: String(lastError?.message || \"delivery_failed\") };\n}\n\nasync function dispatchCommandToAdapter(device, command) {\n const adapter = String(command?.adapter || device?.adapter || \"websocket\").toLowerCase();\n const endpoint = String(device?.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"device_endpoint_missing\");\n if (adapter === \"mqtt\") {\n // MQTT delivery is proxied via a bridge endpoint (HTTP webhook style).\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n type: \"mqtt-command\",\n topic: `castle/devices/${device.id}/commands`,\n action: command.action,\n params: command.params || {}\n })\n });\n if (!res.ok) throw new Error(`mqtt_bridge_http_${res.status}`);\n return { ok: true, adapter, via: \"http-bridge\", status: res.status };\n }\n if (!endpoint.startsWith(\"ws://\") && !endpoint.startsWith(\"wss://\")) throw new Error(\"invalid_websocket_endpoint\");\n const sendResult = await new Promise((resolve, reject) => {\n const ws = new WebSocket(endpoint);\n const timer = setTimeout(() => {\n try {\n ws.close();\n } catch {\n /* noop */\n }\n reject(new Error(\"websocket_timeout\"));\n }, 3000);\n ws.on(\"open\", () => {\n ws.send(\n JSON.stringify({\n type: \"castle-command\",\n commandId: command.id,\n deviceId: command.deviceId,\n action: command.action,\n params: command.params || {}\n }),\n (err) => {\n clearTimeout(timer);\n ws.close();\n if (err) reject(new Error(\"websocket_send_failed\"));\n else resolve({ ok: true, adapter: \"websocket\", via: \"ws\" });\n }\n );\n });\n ws.on(\"error\", () => {\n clearTimeout(timer);\n reject(new Error(\"websocket_connect_failed\"));\n });\n });\n return sendResult;\n}\n\nfunction readHttpJson(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => {\n if (!buf) return resolve({});\n const payload = safeJsonParse(buf);\n if (!payload || typeof payload !== \"object\") return reject(new Error(\"invalid_json_payload\"));\n resolve(payload);\n });\n req.on(\"error\", reject);\n });\n}\n\n/** Ham gövde (HMAC doğrulaması için). */\nfunction readHttpBodyText(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => resolve(buf));\n req.on(\"error\", reject);\n });\n}\n\nfunction maskOpaqueId(value) {\n const t = String(value || \"\");\n if (!t) return \"(empty)\";\n if (t.length <= 8) return \"****\";\n return `${t.slice(0, 4)}…${t.slice(-2)}`;\n}\n\nfunction readGatewayHttpToken(req) {\n const x = String(req.headers?.[\"x-castle-gateway-token\"] || \"\").trim();\n if (x) return x;\n const auth = String(req.headers?.authorization || \"\");\n const m = /^Bearer\\s+(.+)$/i.exec(auth);\n return m ? String(m[1] || \"\").trim() : \"\";\n}\n\nasync function resolveHttpUser(req) {\n const authResult = await verifyClientToken(req);\n if (authResult?.ok && authResult.user?.uid) {\n return { ok: true, uid: String(authResult.user.uid), mode: authResult.kind || \"auth\" };\n }\n if (ALLOW_DEV_ANON && ALLOW_DEV_HTTP_UID) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) return { ok: true, uid: `dev-${devUid.slice(0, 64)}`, mode: \"dev-header\" };\n }\n return { ok: false, reason: authResult?.reason || \"auth_required\" };\n}\n\n/**\n * Epistemic ledger ingest — Firebase uid OR valid transport token + X-Castle-Dev-Uid.\n * Prod disables CASTLE_ALLOW_DEV_HTTP_UID; LLM may still work while telemetry 401'd without this.\n */\nasync function resolveEpistemicIngestActor(req) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok && tok === REQUIRED_GATEWAY_TOKEN) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) {\n return { ok: true, uid: `gw-telemetry-${devUid.slice(0, 64)}`, mode: \"gateway-token-dev-uid\" };\n }\n return { ok: false, reason: \"dev_uid_required\" };\n }\n if (tok) {\n return { ok: false, reason: \"gateway_token_mismatch\" };\n }\n }\n const auth = await resolveHttpUser(req);\n if (auth.ok) return auth;\n return { ok: false, reason: auth.reason || \"auth_required\" };\n}\n\n/** Mesh actor: Firebase uid, dev header, or `guest:<uuid>` via header / `guestId` query (SSE cannot set headers). */\nasync function resolveMeshActor(req) {\n const auth = await resolveHttpUser(req);\n if (auth.ok) return { ok: true, clientUid: auth.uid, mode: auth.mode };\n if (ALLOW_MESH_GUEST) {\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const gid = String(req.headers?.[\"x-castle-guest-id\"] || q.searchParams.get(\"guestId\") || \"\").trim();\n if (gid) {\n const normalized = gid.startsWith(\"guest:\") ? gid.slice(0, 180) : `guest:${gid.slice(0, 160)}`;\n return { ok: true, clientUid: normalized, mode: \"guest-header\" };\n }\n }\n return { ok: false, reason: \"mesh_auth_required\" };\n}\n\n/** Live genesis runtime JSON (GET /rhizoh/genesis/runtime + SSE infra sampler). */\nasync function buildGenesisRuntimeSurfacePayloadLive() {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n let spiralWsActive = 0;\n if (spiralWssForGenesis) {\n for (const c of spiralWssForGenesis.clients) {\n if (c.readyState === 1) spiralWsActive += 1;\n }\n }\n const base = buildGenesisRuntimeSurfacePayload({\n infraMetrics,\n scoredHealth: scored,\n rhizohEnterpriseMetrics,\n mesh: meshContinuityAggregate(),\n workerHealth: worker,\n port: PORT,\n spiralWebSocketClientsActive: spiralWsActive\n });\n return {\n ...base,\n replayFingerprint: computeGenesisReplayFingerprintV0(base),\n gatewayCapabilities: buildGenesisGatewayCapabilitiesV0(),\n genesisStream: {\n schema: \"castle.genesis.stream_cursor.v0\",\n lastAcceptedSeq: getGenesisContinuitySeq(),\n clientIngress: {\n schema: GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n contract: \"castle.world_observation.ingress_envelope.v1\",\n postPath: rhizohRuntime.routes.genesisIngress,\n closure: getGenesisWorldObservationIngressStatusV0()\n },\n seqContinuity: getGenesisContinuitySeqAuditV0(),\n eventArchive: genesisContinuityEventArchiveEnabled()\n ? {\n enabled: true,\n projection: GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION,\n queryPath: rhizohRuntime.routes.genesisContinuityEvents\n }\n : { enabled: false },\n replayRouter: {\n schema: GENESIS_REPLAY_ROUTER_SCHEMA,\n queryPath: rhizohRuntime.routes.genesisReplay,\n queryHint: \"from=&to= or range=from-to; optional type=&checkpoints=0\",\n temporalDiffPath: rhizohRuntime.routes.genesisReplayDiff,\n temporalDiffSchema: GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA,\n equivalencePath: rhizohRuntime.routes.genesisReplayEquivalence,\n analyticsPath: rhizohRuntime.routes.genesisReplayAnalytics,\n evolutionPath: rhizohRuntime.routes.genesisReplayEvolution\n }\n }\n };\n}\n\nconst httpServer = createServer(async (req, res) => {\n const corsAllow = applyHttpCorsHeaders(req, res);\n if (req.method === \"OPTIONS\") {\n if (!corsAllow && req.headers?.origin) {\n console.warn(\"[GATEWAY_CORS] preflight rejected for origin:\", req.headers.origin);\n }\n res.writeHead(corsAllow ? 204 : 403);\n res.end();\n return;\n }\n\n const pathname = getHttpPathname(req);\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/genesis/__ping\") {\n sendJson(res, 200, {\n ok: true,\n pid: process.pid,\n entry: __filename,\n time: Date.now(),\n genesis: true\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/epistemic/__probe\") {\n sendJson(res, 200, {\n ok: true,\n epistemicIngestActorV2: true,\n routes: {\n seal: rhizohRuntime.routes.epistemicSeal,\n logsBatch: rhizohRuntime.routes.epistemicLogsBatch,\n authorityLedgerBatch: rhizohRuntime.routes.authorityLedgerBatch,\n authorityLedgerReplay: rhizohRuntime.routes.authorityLedgerReplay,\n authorityEpochMerge: rhizohRuntime.routes.authorityEpochMerge\n },\n hasGatewayToken: Boolean(REQUIRED_GATEWAY_TOKEN),\n gatewayTokenLen: REQUIRED_GATEWAY_TOKEN ? REQUIRED_GATEWAY_TOKEN.length : 0\n });\n return;\n }\n\n if (\n req.method === \"GET\" &&\n (pathname === \"/rhizoh/live/world-feed\" ||\n pathname === \"/rhizoh/live/sports-bundle\" ||\n pathname === \"/rhizoh/live/news-headlines\")\n ) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_live_world_feed:${ip}`, RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const out =\n pathname === \"/rhizoh/live/sports-bundle\"\n ? await handleLiveSportsBundleGetV0()\n : pathname === \"/rhizoh/live/news-headlines\"\n ? await handleLiveNewsHeadlinesGetV0(req)\n : await handleLiveWorldFeedGetV0(req);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/webhooks/stripe\") {\n await handleStripeMembershipWebhook(req, res);\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/billing/stripe/checkout\") {\n await handleStripeCheckoutCreate(req, res, { readHttpJson, resolveHttpUser, sendJson });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/live\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, live: false, dns: true, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n sendJson(res, 200, { ok: true, live: true, dns: true, service: \"castle-gateway\", ts: Date.now() });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/cors\") {\n const reqOrigin = normalizeHttpOrigin(req.headers?.origin);\n const allowOrigin = accessControlAllowOriginValue(req);\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n requestOrigin: reqOrigin || null,\n allowed: Boolean(allowOrigin),\n allowOrigin: allowOrigin || null,\n allowSetSize: httpCors.allowSet.size\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/ready\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, ready: false, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n try {\n const persistence = getFirebasePersistence();\n const llmProbe = rhizohLlmEnvConfigured();\n sendJson(res, 200, {\n ok: true,\n ready: true,\n dns: true,\n persistence: persistence.mode,\n service: \"castle-gateway\",\n gateway: \"ok\",\n llm: llmProbe ? \"ok\" : \"degraded\",\n spine: llmProbe ? \"ok\" : \"partial\"\n });\n } catch (e) {\n sendJson(res, 503, { ok: false, ready: false, reason: String(e?.message || e), service: \"castle-gateway\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/reality\") {\n const payload = buildRealityHealthPayload();\n sendJson(res, payload.ok ? 200 : 503, payload);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/substrate\") {\n const authority = buildGatewaySubstrateAuthoritySnapshotV0();\n const operational = buildSubstrateOperationalSnapshotV0();\n const drift = buildRealityHealthPayload();\n const ok = Boolean(authority.productionAuthoritySatisfied) && drift.ok;\n sendJson(res, ok ? 200 : 503, {\n ok,\n service: \"castle-gateway\",\n ts: Date.now(),\n authority,\n operational,\n drift: { ok: drift.ok, errors: drift.drift?.errors, warnings: drift.drift?.warnings }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.councilAnomalyReasoning) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_council_anomaly:${ip}`, 30, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const body = await readHttpJson(req);\n const out = handleCouncilAnomalyReasoningPostV0(body);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/substrate/health\") {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const body = await readHttpJson(req);\n const uid = (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestClientSubstrateHealthV0(body, { uid, clientId: uid });\n if (!result.ok) {\n sendJson(res, 400, result);\n return;\n }\n recordSubstrateOtelEventV0(\"client_health_ingest\", { uid: String(uid).slice(0, 32) });\n sendJson(res, 200, result);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/deps\") {\n const t0 = Date.now();\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, {\n ok: false,\n dns: true,\n llm: false,\n firestore: false,\n phase: \"maintenance\",\n latencyMs: Date.now() - t0,\n service: \"castle-gateway\"\n });\n return;\n }\n const persistence = getFirebasePersistence();\n let firestoreOk = persistence.mode !== \"firebase\";\n let firestoreDetail = persistence.mode === \"file\" ? \"file_mode\" : \"not_applicable\";\n if (persistence.mode === \"firebase\" && persistence.db) {\n const pr = await probeFirestoreReachable(persistence.db, 2000);\n firestoreOk = pr.ok;\n firestoreDetail = pr.reason || (pr.ok ? \"reachable\" : \"fail\");\n } else if (persistence.mode === \"firebase\" && !persistence.db) {\n firestoreOk = false;\n firestoreDetail = \"firebase_not_initialized\";\n }\n const llmOk = rhizohLlmEnvConfigured();\n const overallOk = llmOk && firestoreOk;\n const latencyMs = Date.now() - t0;\n sendJson(res, 200, {\n ok: overallOk,\n dns: true,\n llm: llmOk,\n firestore: persistence.mode === \"firebase\" ? firestoreOk : false,\n persistence: persistence.mode,\n firestoreDetail,\n latencyMs,\n service: \"castle-gateway\",\n wsPort: PORT\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health\") {\n const persistence = getFirebasePersistence();\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n wsPort: PORT,\n persistence: persistence.mode,\n presenceMesh: \"sse\"\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/health\") {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n sendJson(res, 200, {\n ok: true,\n role: \"gateway\",\n queueLagMs: infraMetrics.queueLag,\n queueDepth: infraMetrics.queueDepth,\n errors: infraMetrics.errors,\n status: scored.status,\n score: scored.score,\n reasons: scored.reasons,\n worker: worker\n ? {\n status: worker.status,\n score: worker.score,\n reasons: worker.reasons\n }\n : null,\n timestamp: Date.now()\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisRuntime) {\n const payload = await buildGenesisRuntimeSurfacePayloadLive();\n sendJson(res, 200, payload);\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.genesisIngress) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const actor = await resolveMeshActor(req);\n const clientId = actor.ok ? actor.clientUid : (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestGenesisWorldObservationV0(body, { clientId });\n if (!result.ok) {\n const status =\n result.error === \"rate_limited\" || result.error === \"agent_spoke_throttled\"\n ? 429\n : result.error === \"payload_too_large\"\n ? 413\n : 400;\n sendJson(res, status, result);\n return;\n }\n sendJson(res, result.idempotent ? 200 : 202, result);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisStream) {\n const corsOrigin = accessControlAllowOriginValue(req);\n if (!corsOrigin && String(process.env.NODE_ENV || \"\").toLowerCase() === \"production\") {\n const og = String(req.headers?.origin || \"\").trim();\n if (og) {\n console.warn(\"[GATEWAY] genesis SSE: missing Access-Control-Allow-Origin for browser Origin (CORS whitelist mismatch):\", og);\n }\n }\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n res.write(\"retry: 3000\\n\\n\");\n res.write(\": genesis continuity stream\\n\\n\");\n res.write(\"event: genesis\\n\");\n res.write(`data: {\"booted\":true,\"pid\":${process.pid}}\\n\\n`);\n registerGenesisSseClient(res, req);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLatest) {\n const cp = getLatestGenesisSignedCheckpoint();\n if (!cp) {\n sendJson(res, 404, { ok: false, error: \"no_checkpoint_yet\", hint: \"wait_for_seq_interval_or_configure_signing_secret\" });\n return;\n }\n sendJson(res, 200, { ok: true, checkpoint: cp });\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(rhizohRuntime.routes.genesisCheckpointBySeqPrefix)) {\n const tail = pathname.slice(rhizohRuntime.routes.genesisCheckpointBySeqPrefix.length).split(\"/\")[0];\n const seq = parseInt(String(tail || \"\").trim(), 10);\n const out = await genesisCheckpointQueryBySeqV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"checkpoint_not_found\") {\n sendJson(res, 404, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointRange) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryRangeV0(from, to);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"range_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLineage) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const seq = parseInt(String(u.searchParams.get(\"seq\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryLineageV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"lineage_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisContinuityEvents) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const limit = parseInt(String(u.searchParams.get(\"limit\") || \"\").trim(), 10);\n const out = await queryGenesisContinuityEventArchiveV0(from, to, type, limit);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\" || out.error === \"event_archive_disabled\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"archive_file_too_large\") {\n sendJson(res, 413, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplay) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await resolveGenesisReplayRouterV1({\n from: fromSeq,\n to: toSeq,\n type,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayDiff) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const out = await computeGenesisReplayTemporalDiffV1({ from: fromSeq, to: toSeq, type });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEquivalence) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from1 = parseInt(String(u.searchParams.get(\"from1\") || \"\").trim(), 10);\n const to1 = parseInt(String(u.searchParams.get(\"to1\") || \"\").trim(), 10);\n const from2 = parseInt(String(u.searchParams.get(\"from2\") || \"\").trim(), 10);\n const to2 = parseInt(String(u.searchParams.get(\"to2\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await compareGenesisReplayEquivalenceV1({\n from1,\n to1,\n from2,\n to2,\n type,\n includeCheckpoints\n });\n if (\n !out.ok &&\n (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")\n ) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayAnalytics) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayAnalyticsV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEvolution) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const collapseWindows = parseInt(String(u.searchParams.get(\"collapseWindows\") || \"0\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayEvolutionV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n collapseWindows,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/metrics\") {\n res.writeHead(200, { \"Content-Type\": \"text/plain; version=0.0.4; charset=utf-8\" });\n res.end(renderInfraMetricsProm());\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.infraPrometheusMetrics) {\n if (process.env.CASTLE_PROMETHEUS_METRICS !== \"1\") {\n sendJson(res, 404, { ok: false, error: \"prometheus_metrics_disabled\" });\n return;\n }\n res.writeHead(200, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(renderRhizohPrometheusMetrics());\n return;\n }\n if (req.method === \"GET\" && pathname.startsWith(\"/infra/traces/\")) {\n const traceId = decodeURIComponent(pathname.slice(\"/infra/traces/\".length));\n const item = await getTraceById(traceId);\n if (!item) return sendJson(res, 404, { ok: false, error: \"trace_not_found\" });\n return sendJson(res, 200, { ok: true, trace: item });\n }\n if (req.method === \"GET\" && pathname === \"/infra/traces\") {\n const u = new URL(String(req.url || \"/infra/traces\"), `http://${req.headers.host || \"localhost\"}`);\n const sessionId = String(u.searchParams.get(\"sessionId\") || \"\").trim();\n const items = sessionId ? await listTracesBySession(sessionId) : [];\n return sendJson(res, 200, { ok: true, sessionId, traces: items });\n }\n\n /** Presence room mesh (C): JOIN / LEAVE / SNAPSHOT / DELTA / REPLAY (HTTP); SUBSCRIBE = SSE stream. */\n if (req.method === \"POST\" && pathname === \"/presence/mesh/join\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid, clientUid: actor.clientUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/leave\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshLeave(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/snapshot\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n sendJson(res, 200, meshSnapshot(roomUid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/delta\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, MAX_MESSAGE_BYTES);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const seq = meshAppendDelta(roomUid, actor.clientUid, {\n node: body?.node,\n projectionPatch: body?.projectionPatch,\n writerSubject: body?.writerSubject\n });\n sendJson(res, 200, { ok: true, roomUid, seq });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/replay\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const fromNodeId = body?.fromNodeId != null ? String(body.fromNodeId) : \"\";\n const fromSeq = body?.fromSeq;\n if (fromNodeId) {\n sendJson(res, 200, meshReplayFromNodeId(roomUid, fromNodeId));\n return;\n }\n sendJson(res, 200, meshReplayFromSeq(roomUid, fromSeq));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/presence/mesh/subscribe\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const roomUid = String(q.searchParams.get(\"roomUid\") || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n const write = (chunk) => {\n try {\n res.write(chunk);\n } catch {\n /* closed */\n }\n };\n const unsub = meshSubscribeSse(roomUid, write);\n const onClose = () => {\n unsub();\n meshLeave(roomUid, actor.clientUid);\n try {\n res.end();\n } catch {\n /* ignore */\n }\n };\n req.on(\"close\", onClose);\n req.on(\"aborted\", onClose);\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, uid: auth.uid, mode: auth.mode, items: await listConnections(auth.uid) });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const id = await createConnection(auth.uid, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"create_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n await updateConnection(auth.uid, id, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"update_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"DELETE\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n await deleteConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"delete_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url?.startsWith(\"/rhizoh/network/presence\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const u = new URL(req.url, \"http://localhost\");\n const roomKey = String(u.searchParams.get(\"room\") || \"world_space_c2c_v0\").slice(0, 64);\n sendJson(res, 200, {\n ok: true,\n roomKey,\n presence: listCastleNetworkPresenceV0(roomKey)\n });\n } catch (error) {\n sendJson(res, 500, { ok: false, error: error?.message || \"presence_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/rhizoh/sync/vault\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const snapshot = await getCloudSyncSnapshotV0(auth.uid);\n sendJson(res, 200, { ok: true, uid: auth.uid, snapshot });\n } catch (error) {\n sendJson(res, 500, { ok: false, error: error?.message || \"cloud_sync_pull_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/rhizoh/sync/vault\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const snapshot = await mergeCloudSyncSnapshotV0(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, uid: auth.uid, snapshot });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"cloud_sync_push_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections/test\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n if (!checkHttpRateLimit(`llm_conn_test:${auth.uid}`, RL_LLM_CONN_TEST_PER_MIN, 60_000)) {\n return sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n }\n const hasBodyKey = !!String(payload?.apiKey || \"\").trim();\n const result = await queryRhizohLlm(\n {\n message: String(payload?.message || \"Ping from Castle Gateway\"),\n context: payload?.context || { source: \"connection_test\" },\n provider: payload?.provider,\n model: payload?.model,\n apiKey: payload?.apiKey || \"\"\n },\n { allowExternalApiKey: true, llmKeySource: hasBodyKey ? \"auto\" : \"env\" }\n );\n logLlmAccess({\n route: \"/llm/connections/test\",\n uid: auth.uid,\n llmKeyBillingOwner: result.llmKeyBillingOwner,\n llmKeyOrigin: result.llmKeyOrigin,\n provider: result.provider,\n model: result.model,\n connectionId: null\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"connection_test_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAgentIdentities(auth.uid);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerAgentIdentity({\n ownerUid: auth.uid,\n agentId: payload?.agentId,\n role: payload?.role,\n personaSeed: payload?.personaSeed || {},\n capabilityLevel: payload?.capabilityLevel\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/agents/identities/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const owns = await getAgentIdentity(id);\n if (!owns || owns.ownerUid !== auth.uid) return sendJson(res, 403, { ok: false, error: \"forbidden\" });\n const payload = await readHttpJson(req);\n const row = await updateAgentIdentity(id, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/academy/events\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/academy/events\"}`);\n const status = String(url.searchParams.get(\"status\") || \"\");\n const items = await listAcademyEvents({ ownerUid: auth.uid, status, limit: 120 });\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/academy/events\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const ev = await queueAcademyEvent({\n type: payload?.type,\n roomId: payload?.roomId,\n ownerUid: auth.uid,\n participants: payload?.participants || [],\n topic: payload?.topic || \"academy\",\n payload: payload?.payload || {}\n });\n sendJson(res, 200, { ok: true, event: ev });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && String(req.url || \"\").startsWith(\"/academy/events/\") && String(req.url || \"\").endsWith(\"/resolve\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const parts = String(req.url || \"\").split(\"/\").filter(Boolean);\n const eventId = parts[2] || \"\";\n try {\n const payload = await readHttpJson(req);\n const out = await resolveAcademyEvent(eventId, payload || {});\n sendJson(res, 200, { ok: true, event: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_resolve_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/memory\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/memory\"}`);\n const scope = url.searchParams.get(\"scope\") === \"agents\" ? \"agents\" : \"users\";\n const id = String(url.searchParams.get(\"id\") || (scope === \"agents\" ? \"\" : auth.uid));\n const kind = String(url.searchParams.get(\"kind\") || \"\");\n const limit = Number(url.searchParams.get(\"limit\") || 40);\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n sendJson(res, 200, { ok: true, scope, id, kind, items: await listMemories({ scope, id, kind, limit }) });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const profile = await getPersonaGoalMemory(auth.uid);\n sendJson(res, 200, { ok: true, profile });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const profile = await setPersonaGoalMemory({\n uid: auth.uid,\n personaPatch: payload?.personaPatch || {},\n goals: Array.isArray(payload?.goals) ? payload.goals : null,\n preferencesPatch: payload?.preferencesPatch || {}\n });\n sendJson(res, 200, { ok: true, profile });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"profile_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/compact\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const out = await autoCompactMemories({ scope, id });\n sendJson(res, 200, { ok: true, scope, id, ...out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_compact_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/context\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const context = await getMemoryContext({\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n query: String(payload?.query || \"\"),\n limit: Number(payload?.limit || 80)\n });\n sendJson(res, 200, { ok: true, context });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_context_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/place-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const placeName = String(payload?.placeName || \"\").trim();\n if (!placeName) return sendJson(res, 400, { ok: false, error: \"placeName_required\" });\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const persona = String(payload?.waypointPersona || payload?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(placeName)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Place: ${placeName}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Give a concise conversational briefing about this place for a walking-route companion.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-place-brief\", placeName, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `PLACE_BRIEF:${placeName} => ${String(summary.reply || \"\").slice(0, 900)}`,\n tags: [\"place\", \"brief\", \"event-layer\"],\n importance: 0.62,\n meta: { placeName }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"place-brief\",\n text: `${placeName} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: {\n placeName,\n directive: summary.directive,\n lat: hit?.lat ? Number(hit.lat) : null,\n lon: hit?.lon ? Number(hit.lon) : null\n }\n });\n sendJson(res, 200, { ok: true, place: hit || null, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"place_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/route-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const waypoints = Array.isArray(payload?.waypoints) ? payload.waypoints.slice(0, 24) : [];\n const out = [];\n for (const wp of waypoints) {\n const name = String(wp?.name || \"\").trim();\n if (!name) continue;\n const persona = String(wp?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(name)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Waypoint: ${name}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Generate a concise spoken briefing for route waypoint.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-route-brief\", waypoint: name, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n out.push({\n name,\n persona,\n place: hit || null,\n reply: summary.reply,\n directive: summary.directive\n });\n }\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `ROUTE_BRIEF: waypoints=${out.length} ${out.map((x) => `${x.name}:${x.persona}`).join(\" | \")}`.slice(0, 1300),\n tags: [\"route\", \"waypoint\", \"brief\", \"event-layer\"],\n importance: 0.74,\n meta: { count: out.length }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"route-brief\",\n text: out.map((x) => `${x.name}:${String(x.reply || \"\").slice(0, 140)}`).join(\" | \").slice(0, 1400),\n roomId: \"studio-main\",\n meta: {\n waypointCount: out.length,\n waypoints: out.map((x) => ({\n name: x.name,\n persona: x.persona,\n lat: x.place?.lat ? Number(x.place.lat) : null,\n lon: x.place?.lon ? Number(x.place.lon) : null\n }))\n }\n });\n sendJson(res, 200, { ok: true, items: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"route_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/pdf-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const title = String(payload?.title || \"document\");\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const providedText = String(payload?.text || \"\");\n const url = String(payload?.url || \"\");\n let content = providedText;\n if (!content && url) content = await fetchTextFromUrl(url);\n if (!content) return sendJson(res, 400, { ok: false, error: \"text_or_supported_url_required\" });\n const summary = await queryRhizohLlm(\n {\n message: `Summarize this document and provide key points + practical actions.\\nTitle:${title}\\nContent:\\n${content.slice(0, 12000)}`,\n context: { source: \"event-layer-pdf-brief\", title },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `DOC_BRIEF:${title} => ${String(summary.reply || \"\").slice(0, 1000)}`,\n tags: [\"document\", \"brief\", \"library\", \"event-layer\"],\n importance: 0.7,\n meta: { title, url: url || null }\n });\n await addTranscript(auth.uid, {\n source: \"library-brief\",\n eventType: \"pdf-brief\",\n text: `${title} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { title }\n });\n sendJson(res, 200, { ok: true, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/pdf/upload\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req, 8 * 1024 * 1024);\n const job = await enqueuePdfJob({\n uid: auth.uid,\n fileName: payload?.fileName || \"document.pdf\",\n bytesBase64: payload?.contentBase64\n });\n sendJson(res, 200, { ok: true, job });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_upload_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/session\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createPublishSession(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session\",\n text: `Publish session armed: ${row.protocol} ${row.target}`.slice(0, 500),\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: row.id, bridge: row.bridge }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/studio/publish/sessions\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listPublishSessions(auth.uid, 80);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/health-check\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const protocol = String(payload?.protocol || \"WHIP\");\n const target = String(payload?.target || \"\");\n const health = await checkPublishTargetHealth(protocol, target);\n if (payload?.sessionId) {\n await updatePublishSession(auth.uid, String(payload.sessionId), {\n status: health.ok ? \"healthy\" : \"degraded\",\n reconnectPolicy: {\n enabled: true,\n backoffSec: [2, 5, 10, 20],\n nextRetryInSec: health.reconnectInSec || 0,\n maxRetries: 8,\n reason: health.reason\n }\n });\n }\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-health\",\n text: `${protocol} ${target} => ${health.ok ? \"healthy\" : \"degraded\"} (${health.reason})`,\n roomId: \"studio-main\",\n meta: { protocol, target, reconnect: !!health.shouldReconnect }\n });\n sendJson(res, 200, { ok: true, health });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_health_check_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/studio/publish/sessions/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updatePublishSession(auth.uid, id, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session-update\",\n text: `Session ${id} -> ${row.status || \"updated\"}`,\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: id, status: row.status, ingestUrl: row.ingestUrl || \"\" }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/transcripts\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/transcripts\"}`);\n const limit = Math.max(1, Math.min(200, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await listTranscripts(auth.uid, limit);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/transcripts\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await addTranscript(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"transcript_write_failed\" });\n }\n return;\n }\n\n /** E1: broadcast intent → GreenRoom capability → transcript + memory artifact */\n if (req.method === \"POST\" && req.url === \"/studio/capabilities/greenroom\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const traceId = String(payload?.traceId || `TRC-${Date.now().toString(36).toUpperCase()}`);\n const title = String(payload?.title || \"Castle Live Broadcast\").slice(0, 200);\n const intentRaw = String(payload?.intentRaw || \"\").slice(0, 800);\n const audienceEstimate = Math.max(0, Math.min(50_000_000, Number(payload?.audienceEstimate) || 1200));\n const roomId = String(payload?.roomId || \"greenroom-main\");\n const transcriptRow = await addTranscript(auth.uid, {\n source: \"rhizoh\",\n eventType: \"BROADCAST_ROUTED\",\n text: `GreenRoom · ${title}${intentRaw ? `\\nIntent: ${intentRaw}` : \"\"}`,\n roomId,\n meta: {\n ack: \"BROADCAST_ROUTED\",\n capability: \"greenroom\",\n intentType: \"broadcast\",\n traceId,\n audienceEstimate,\n artifact: {\n kind: \"broadcast_card\",\n title,\n traceId,\n roomId,\n createdAt: Date.now()\n }\n }\n });\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n text: `GREENROOM_BROADCAST:${title} trace=${traceId}`,\n tags: [\"broadcast\", \"greenroom\", \"artifact\", \"castle-library\"],\n importance: 0.72,\n kind: \"episodic\",\n meta: { traceId, capability: \"greenroom\", transcriptId: transcriptRow.id }\n });\n sendJson(res, 200, {\n ok: true,\n ack: \"BROADCAST_ROUTED\",\n traceId,\n transcript: transcriptRow,\n replayPath: `/replay/${encodeURIComponent(traceId)}`,\n sharePath: `/replay/${encodeURIComponent(traceId)}`\n });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"greenroom_capability_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/metadata/search\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/metadata/search\"}`);\n const q = String(url.searchParams.get(\"q\") || \"\");\n const limit = Math.max(1, Math.min(300, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await searchMetadata(auth.uid, q, limit);\n sendJson(res, 200, { ok: true, q, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerDevice(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, device: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"device_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const lat = Number(payload?.lat);\n const lon = Number(payload?.lon);\n const speed = Math.max(0, Number(payload?.speed || 0));\n const cfgCenterLat = Number(process.env.CASTLE_GEOFENCE_CENTER_LAT || 41.0082);\n const cfgCenterLon = Number(process.env.CASTLE_GEOFENCE_CENTER_LON || 28.9784);\n const cfgRadiusM = Math.max(100, Number(process.env.CASTLE_GEOFENCE_RADIUS_M || 55000));\n const cfgSpeedCap = Math.max(1, Number(process.env.CASTLE_SPEED_CAP_MPS || 35));\n const dist = Number.isFinite(lat) && Number.isFinite(lon) ? haversineMeters(cfgCenterLat, cfgCenterLon, lat, lon) : 0;\n const geofenceOk = !Number.isFinite(dist) ? true : dist <= cfgRadiusM;\n const speedOk = speed <= cfgSpeedCap;\n const failsafe = !(geofenceOk && speedOk);\n const safety = {\n geofenceOk,\n speedOk,\n failsafe,\n distanceFromCenterM: Number.isFinite(dist) ? dist : null,\n speedCapMps: cfgSpeedCap\n };\n const row = await appendTelemetry(auth.uid, deviceId, {\n lat,\n lon,\n speed,\n battery: payload?.battery,\n mode: payload?.mode || (failsafe ? \"FAILSAFE_HOLD\" : \"ACTIVE\"),\n raw: payload?.raw || {}\n });\n await updateDevice(auth.uid, deviceId, {\n status: failsafe ? \"failsafe\" : \"active\",\n lastTelemetryAt: row.ts,\n safetyState: safety\n });\n if (failsafe) {\n await addTranscript(auth.uid, {\n source: \"robotics-guardrails\",\n eventType: \"failsafe\",\n text: `Device ${deviceId} failsafe triggered geofenceOk=${geofenceOk} speedOk=${speedOk}`,\n roomId: \"studio-main\",\n meta: { deviceId, safety }\n });\n }\n sendJson(res, 200, { ok: true, telemetry: row, safety, command: failsafe ? \"FAILSAFE_HOLD\" : \"CONTINUE\" });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"telemetry_ingest_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listTelemetry(auth.uid, 150);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createAutomation(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"automation-scheduled\",\n text: `Automation scheduled: ${row.title} at ${new Date(row.scheduleAt).toLocaleString()}`,\n roomId: \"studio-main\",\n meta: { automationId: row.id, action: row.action, deviceId: row.deviceId }\n });\n sendJson(res, 200, { ok: true, automation: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"automation_create_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAutomations(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listDevices(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const devs = await listDevices(auth.uid, 500);\n const d = devs.find((x) => x.id === deviceId);\n if (!d) return sendJson(res, 404, { ok: false, error: \"device_not_found\" });\n const action = String(payload?.action || \"\");\n const caps = Array.isArray(d.capabilityProfile) ? d.capabilityProfile : [];\n if (action && caps.length && !caps.includes(action) && !caps.includes(\"*\")) {\n return sendJson(res, 400, { ok: false, error: \"unsupported_action_for_device\", capabilityProfile: caps });\n }\n const cmd = await enqueueCommand(auth.uid, {\n deviceId,\n action: action || \"NOOP\",\n params: payload?.params || {},\n adapter: payload?.adapter || d.adapter || \"websocket\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-command-queue\",\n eventType: \"command-queued\",\n text: `${cmd.deviceId} -> ${cmd.action}`,\n roomId: \"studio-main\",\n meta: { commandId: cmd.id, adapter: cmd.adapter }\n });\n sendJson(res, 200, { ok: true, command: cmd });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listCommands(auth.uid, 200);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/robotics/commands/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updateCommandStatus(auth.uid, id, payload?.status || \"sent\");\n sendJson(res, 200, { ok: true, command: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/rhizoh/bridge\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const result = await runRhizohBrainV2({\n message: String(payload?.message || \"robotics bridge command\"),\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n worldState: payload?.worldState || {},\n deviceState: payload?.deviceState || {},\n intent: \"robotics-mechanics\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-bridge\",\n eventType: \"robotics-command\",\n text: `Bridge -> ${String(result.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { directive: result.directive, agentId: payload?.agentId || \"\" }\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"robotics_bridge_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/planner/graph\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const graph = buildTaskActionGraph(String(payload?.task || \"\"), payload?.context || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"planner-graph\",\n text: `Planner graph built: ${graph.taskTitle} nodes=${graph.nodes.length}`,\n roomId: \"studio-main\",\n meta: { taskTitle: graph.taskTitle, nodeCount: graph.nodes.length }\n });\n sendJson(res, 200, { ok: true, graph });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"planner_graph_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerSocialChannel(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, channel: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_channel_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listSocialChannels(auth.uid, 100);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/broadcast\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const text = String(payload?.text || \"\").slice(0, 1200);\n const channels = await listSocialChannels(auth.uid, 100);\n const enabled = channels.filter((c) => c.status === \"enabled\");\n const deliveries = [];\n for (const c of enabled) {\n const out = await deliverSocialWithRetry(c, text);\n deliveries.push({ id: c.id, platform: c.platform, ...out });\n }\n const okCount = deliveries.filter((d) => d.ok).length;\n await addTranscript(auth.uid, {\n source: \"social-layer\",\n eventType: \"social-broadcast\",\n text: `Broadcast delivered ${okCount}/${enabled.length} channel(s): ${text.slice(0, 240)}`,\n roomId: \"studio-main\",\n meta: { deliveries }\n });\n sendJson(res, 200, { ok: true, sentChannels: okCount, totalChannels: enabled.length, deliveries });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_broadcast_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/ethics/programs\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, ...buildEthicsPrograms() });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands/dispatch\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const all = await listCommands(auth.uid, 200);\n const queued = all.filter((c) => String(c.status || \"queued\") === \"queued\").slice(0, 50);\n const devices = await listDevices(auth.uid, 500);\n const byId = new Map(devices.map((d) => [d.id, d]));\n const results = [];\n for (const cmd of queued) {\n const dev = byId.get(cmd.deviceId);\n if (!dev) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: \"device_not_found\" });\n continue;\n }\n try {\n const dispatched = await dispatchCommandToAdapter(dev, cmd);\n await updateCommandStatus(auth.uid, cmd.id, \"sent\");\n results.push({ id: cmd.id, ok: true, adapter: dispatched.adapter, via: dispatched.via });\n } catch (error) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: String(error?.message || \"dispatch_failed\") });\n }\n }\n await addTranscript(auth.uid, {\n source: \"robotics-command-dispatch\",\n eventType: \"command-dispatch-batch\",\n text: `Dispatch batch size=${results.length} ok=${results.filter((x) => x.ok).length}`,\n roomId: \"studio-main\",\n meta: { results: results.slice(0, 30) }\n });\n sendJson(res, 200, { ok: true, processed: results.length, results });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"dispatch_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/pdf/jobs/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n const job = await getPdfJob(auth.uid, id);\n if (!job) return sendJson(res, 404, { ok: false, error: \"pdf_job_not_found\" });\n sendJson(res, 200, { ok: true, job });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const row = await appendMemory({\n scope,\n id,\n text: payload?.text,\n tags: payload?.tags || [],\n importance: payload?.importance,\n kind: payload?.kind || \"episodic\",\n meta: { uid: auth.uid, ...(payload?.meta || {}) }\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.externalTruth) {\n try {\n if (process.env.RHIZOH_EXTERNAL_TRUTH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_truth:${ip}`, RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n sendJson(res, 200, buildRhizohExternalGroundTruthPayload(req));\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.productOutcome) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const rawBody = await readHttpBodyText(req, 64 * 1024);\n const body = rawBody.trim() ? safeJsonParse(rawBody) : null;\n if (!body || typeof body !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"invalid_json_payload\" });\n return;\n }\n const sigHdr = String(req.headers?.[\"x-rhizoh-outcome-signature\"] || \"\").trim();\n const hmacV = verifyRhizohOutcomeHmac(rawBody, sigHdr);\n if (!hmacV.ok) {\n sendJson(res, 401, { ok: false, error: hmacV.error || \"outcome_hmac_invalid\" });\n return;\n }\n const srcTokHdr = String(req.headers?.[\"x-rhizoh-outcome-source-token\"] || \"\").trim();\n const srcV = verifyRhizohOutcomeSourceToken(body.source, srcTokHdr);\n if (!srcV.ok) {\n sendJson(res, 401, { ok: false, error: srcV.error || \"outcome_source_token_invalid\" });\n return;\n }\n const subjectKey = String(body.subjectKey || \"\").trim().slice(0, 128);\n if (\n subjectKey &&\n !checkHttpRateLimit(\n `rhizoh_product_outcome_subj:${subjectKey}`,\n RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN,\n 60_000\n )\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_subject\" });\n return;\n }\n const result = ingestRhizohProductOutcomeHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n schemaVersion: \"1.1.0\",\n aggregate: result.aggregate,\n integrity: result.integrity\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"outcome_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.productOutcomeAggregate) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome_agg:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const u = new URL(String(req.url || \"/\"), \"http://localhost\");\n const cohortId = u.searchParams.get(\"cohortId\") || \"\";\n const decisionFingerprint = u.searchParams.get(\"decisionFingerprint\") || \"\";\n const limit = u.searchParams.get(\"limit\") || \"50\";\n const rows = listRhizohProductOutcomeAggregates({\n cohortId: cohortId || undefined,\n decisionFingerprint: decisionFingerprint || undefined,\n limit: Number(limit) || 50\n });\n sendJson(res, 200, { ok: true, schemaVersion: \"1.1.0\", rows });\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.externalLossBatch) {\n try {\n if (process.env.RHIZOH_EXTERNAL_LOSS_BATCH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_loss_batch:${ip}`, RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const result = ingestRhizohExternalLossBatchHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, { ok: true, accepted: result.accepted ?? 0 });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"batch_failed\") });\n }\n return;\n }\n\n function moderationAdminOk(req) {\n const expected = String(process.env.CASTLE_MODERATION_ADMIN_KEY || \"\").trim();\n if (!expected) return false;\n const got = String(req.headers[\"x-castle-moderation-key\"] || \"\").trim();\n return got.length > 0 && got === expected;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.opsAbuseReport) {\n try {\n const body = await readHttpJson(req, 32 * 1024);\n const auth = await resolveHttpUser(req);\n const entry = submitAbuseReportV0({\n reporterUid: auth.ok ? auth.uid : null,\n targetUid: body?.targetUid ?? null,\n category: body?.category,\n detail: body?.detail,\n traceId: body?.traceId ?? null\n });\n sendJson(res, 202, { ok: true, reportId: entry.id, status: entry.status });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsModerationQueue) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n queue: listModerationQueueV0(100),\n softBlocked: listSoftBlockedUsersV0()\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-block\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const out = softBlockUserV0(body?.uid, body?.reason);\n sendJson(res, out.ok ? 200 : 400, out);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-unblock\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n sendJson(res, 200, removeSoftBlockV0(body?.uid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsAgentSnapshots) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n const limit = Number(new URL(req.url || \"\", \"http://local\").searchParams.get(\"limit\") || 50);\n sendJson(res, 200, { ok: true, snapshots: listRecentAgentSnapshotsV0(limit) });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.academicObservatoryExport) {\n const auth = await resolveHttpUser(req);\n const out = handleAcademicObservatoryExportGetV0(req, auth);\n sendJson(res, out.status, out.body);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsHardeningStatus) {\n const auth = await resolveHttpUser(req);\n const principal = auth.ok ? `uid:${auth.uid}` : null;\n const gclSnap = await getGlobalCostLedgerSnapshotV0(principal);\n const economicStrategy = evaluateEconomicStrategyL3V0(gclSnap);\n const hardeningTenantId = resolveHardeningTenantIdV0(auth, req);\n const unifiedState = await buildUnifiedStateNarrativeV0({\n tenantId: hardeningTenantId ?? undefined,\n platformScope: !hardeningTenantId,\n principal\n });\n sendJson(res, 200, {\n ok: true,\n schema: \"rhizoh.ops.hardening_status.v0\",\n containment: readAgentContainmentConfigV0(),\n cost: readCostContainmentConfigV0(),\n gcl: {\n backend: await getGclBackendStatusV0(),\n snapshot: gclSnap\n },\n economicStrategyL3: economicStrategy,\n phasedRollout: await getPhasedRolloutStatsV0(),\n tier: readPhasedRolloutTierV0(),\n principalCost: auth.ok ? await getCostContainmentStatsV0(principal) : null,\n stressTaxonomy: verifyStressTaxonomyCoverageV0(),\n stressConflictResolution: verifyStressConflictResolutionV0(),\n resolutionStabilityEnvelope: verifyResolutionStabilityEnvelopeV0(),\n unifiedState: {\n tenantScope: {\n tenantId: unifiedState.tenantScope?.tenantId,\n scope: unifiedState.tenantScope?.scope,\n noGlobalDerivedState: unifiedState.tenantScope?.noGlobalDerivedState\n },\n narrativeFingerprint: unifiedState.narrativeFingerprint,\n screenshotScopeWatermark: unifiedState.screenshotScopeWatermark,\n interpretationSafetyContract: unifiedState.interpretationSafetyContract,\n interpretationUxContract: {\n schema: unifiedState.interpretationUxContract?.schema,\n layerOrder: unifiedState.interpretationUxContract?.layerOrder,\n narrativeNeverVisuallyDominant: unifiedState.interpretationUxContract?.narrativeNeverVisuallyDominant\n },\n governance: {\n narrativeIsNotDecisionLayer: unifiedState.governance.narrativeIsNotDecisionLayer,\n displayOrder: unifiedState.governance.displayOrder,\n trustPosture: unifiedState.governance.trustPosture,\n interpretationUxContract: unifiedState.governance.interpretationUxContract?.schema\n },\n humanOps: {\n decisionTier: unifiedState.humanOps?.humanDecisionScaling?.tier,\n decisionOwner: unifiedState.humanOps?.humanDecisionScaling?.decisionOwner,\n opsRouteId: unifiedState.humanOps?.humanDecisionOpsRunbook?.routing?.routeId,\n slaAckMinutes: unifiedState.humanOps?.humanDecisionOpsRunbook?.sla?.ackMinutes,\n uiDisclaimerRequired: unifiedState.humanOps?.narrativeUiSafety?.displayRules?.requireDisclaimerBanner,\n misreadHighResidual: unifiedState.humanOps?.misreadSimulation?.highResidualCount,\n propagationHighResidual: unifiedState.humanOps?.socialPropagationSimulation?.highResidualCount,\n worstWatermarkSurvivability:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.worstWatermarkSurvivability,\n dominantDistortionSource:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.dominantDistortionSource\n },\n culturalRisk: {\n perceptualRiskClass: unifiedState.culturalRisk?.perceptualRiskClass,\n trustFork: unifiedState.culturalRisk?.trustDynamics?.fork,\n trustDecayScore: unifiedState.culturalRisk?.trustDynamics?.scores?.trustDecay,\n mythologyScore: unifiedState.culturalRisk?.trustDynamics?.scores?.mythology,\n confidenceDetachmentExposure:\n unifiedState.culturalRisk?.trustDynamics?.axes?.confidenceDetachmentExposure\n },\n appliedSystemsLayer: {\n schema: unifiedState.appliedSystemsLayer?.schema,\n researchOutputType: unifiedState.appliedSystemsLayer?.researchGovernance?.outputType,\n roboticsBlockActuation: unifiedState.appliedSystemsLayer?.roboticsGrounding?.blockActuation,\n spiralRumorCount:\n unifiedState.appliedSystemsLayer?.spiralMMOGameKernel?.gameLayer?.rumors?.length\n },\n actionSemanticGovernance: {\n schema: unifiedState.actionSemanticGovernance?.schema,\n highRiskActionCount: unifiedState.actionSemanticGovernance?.highRiskActionCount,\n loopClosed: unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.loopClosed,\n authorityCollapseMitigated:\n unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.risks?.authorityCollapse\n ?.mitigated\n },\n actionContextResolution: {\n schema: unifiedState.actionContextResolution?.schema,\n contextFingerprint: unifiedState.actionContextResolution?.contextFingerprint?.fingerprintHash,\n domainCollision: unifiedState.actionContextResolution?.domainCollision?.detected,\n deployAgentSpiralBlocked:\n unifiedState.actionContextResolution?.actionOverloadingRisk?.deployAgentExample\n ?.spiral_mmo === \"blocked\",\n roboticsActuationEligible:\n unifiedState.actionContextResolution?.roboticsActuationCorrectness?.actuationEligible\n },\n epistemicCoherence: {\n schema: unifiedState.epistemicCoherence?.schema,\n verdict: unifiedState.epistemicCoherence?.systemCoherence?.verdict,\n coherenceScore: unifiedState.epistemicCoherence?.systemCoherence?.score,\n contradictionCount: unifiedState.epistemicCoherence?.crossLayerContradictions?.count,\n operatorDecision: unifiedState.epistemicCoherence?.uxCompression?.operatorDecision,\n headlineTr: unifiedState.epistemicCoherence?.uxCompression?.headline?.tr,\n falseSafetyWarning: Boolean(\n unifiedState.epistemicCoherence?.uxCompression?.falseSafetyWarning\n )\n },\n coherenceAuthorityBoundary: {\n schema: unifiedState.coherenceAuthorityBoundary?.schema,\n bindingValid: unifiedState.coherenceAuthorityBoundary?.eclAuthorityBinding?.valid,\n canExecute: unifiedState.coherenceAuthorityBoundary?.contract?.can_execute,\n mandatoryBannerTr:\n unifiedState.coherenceAuthorityBoundary?.disclaimers?.mandatoryBanner?.tr\n },\n decisionLatencyGovernance: {\n schema: unifiedState.decisionLatencyGovernance?.schema,\n latencyTier: unifiedState.decisionLatencyGovernance?.latencyTier,\n inflationScore: unifiedState.decisionLatencyGovernance?.latencyInflation?.score,\n fastPathEligible: unifiedState.decisionLatencyGovernance?.fastPath?.eligible,\n primaryActionId: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.primaryActionId,\n ackSlaMinutes: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.ackSlaMinutes,\n certaintyCap: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.certaintyCap,\n uncertaintyItems:\n unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.uncertaintyEnvelope?.itemCount\n },\n decisionPacketUncertaintyBoundary: {\n schema: unifiedState.decisionPacketUncertaintyBoundary?.schema,\n invariantValid: unifiedState.decisionPacketUncertaintyBoundary?.invariantCheck?.valid,\n centralInvariant: unifiedState.decisionPacketUncertaintyBoundary?.centralInvariant\n },\n epistemicDecisionPacing: {\n schema: unifiedState.epistemicDecisionPacing?.schema,\n queueSaturationTier:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation?.tier,\n operatorProcessingLatency:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation\n ?.operatorProcessingLatency,\n postureWindowCount:\n unifiedState.epistemicDecisionPacing?.temporaryStaticPostureWindows\n ?.temporaryStaticPostureWindows?.windowCount,\n executionBoundariesValid:\n unifiedState.epistemicDecisionPacing?.executionBoundaries?.valid,\n grantsExecutionApproval: unifiedState.epistemicDecisionPacing?.grantsExecutionApproval\n },\n epistemicTemporalCoherence: {\n schema: unifiedState.epistemicTemporalCoherence?.schema,\n allInvariantsValid: unifiedState.epistemicTemporalCoherence?.allInvariantsValid,\n temporalAlignmentValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.temporalAlignment?.valid,\n crossWindowGuardValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard?.valid,\n delayedTruthValid: unifiedState.epistemicTemporalCoherence?.invariants?.delayedTruthRule?.valid,\n reconciliationRequired:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard\n ?.reconciliation?.required\n },\n realityDriftObserver: {\n schema: unifiedState.realityDriftObserver?.schema,\n driftActive: unifiedState.realityDriftObserver?.driftObserverSummary?.active,\n misapprehensionShapeCount:\n unifiedState.realityDriftObserver?.misapprehensionShapeCatalog?.shapeCount,\n criticalShapeCount:\n unifiedState.realityDriftObserver?.driftObserverSummary?.criticalShapeCount,\n roboticsMismatchActive:\n unifiedState.realityDriftObserver?.roboticsFeedbackMismatch?.active\n },\n driftCausality: {\n schema: unifiedState.driftCausality?.schema,\n shapesWithCausalExplanation: unifiedState.driftCausality?.rdolLinkage?.shapesWithCausalExplanation,\n activeDomainCount: unifiedState.driftCausality?.causalitySummary?.activeDomainCount,\n propagationCausalityActive: unifiedState.driftCausality?.causalDomains?.propagation?.active\n },\n productReadiness: unifiedState.productReadiness,\n stateLayers: {\n raw: unifiedState.stateLayers.raw,\n derived: {\n systemState: unifiedState.stateLayers.derived.systemState,\n narrative: unifiedState.stateLayers.derived.narrative,\n validation: unifiedState.stateLayers.derived.validation\n },\n policy: {\n binding: unifiedState.stateLayers.policy.binding,\n decisionOwner: unifiedState.stateLayers.policy.decisionOwner,\n prohibitedActions: unifiedState.stateLayers.policy.prohibitedActions\n }\n },\n systemState: unifiedState.systemState,\n interpretation: unifiedState.interpretation,\n validation: {\n trustPosture: unifiedState.validation.trustPosture,\n adjustedConfidence: unifiedState.validation.adjustedConfidence,\n agreement: unifiedState.validation.confidenceDecomposition.agreement,\n divergenceScore: unifiedState.validation.divergence.divergenceScore,\n validated: unifiedState.validation.divergence.validated,\n uncertaintyTags: unifiedState.validation.uncertainty.tags.map((t) => t.id),\n flags: unifiedState.validation.divergence.flags.map((f) => f.id)\n },\n compression: unifiedState.compression\n }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 6 * 1024 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (\n !checkHttpRateLimit(`rhizoh_voice_transcribe:${rlKey}`, RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN, 60_000)\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const voiceEnv = rhizohVoiceTranscribeEnvV3();\n if (!voiceEnv.any) {\n sendJson(res, 503, {\n ok: false,\n error: \"voice_asr_not_configured\",\n env: voiceEnv\n });\n return;\n }\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n const result = await runRhizohVoiceTranscribeV3(\n applyLanguagePropagationToVoiceBodyV1(payload, langProp)\n );\n sendJson(res, result.ok ? 200 : 422, {\n ...result,\n traceId: String(payload?.traceId || \"\"),\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n atMs: Date.now()\n });\n } catch (error) {\n sendJson(res, 500, {\n ok: false,\n error: \"voice_transcribe_failed\",\n detail: String(error?.message || error),\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(\"/rhizoh/llm/task/\")) {\n try {\n const rawTaskId = decodeURIComponent(pathname.slice(\"/rhizoh/llm/task/\".length).split(\"/\")[0] || \"\");\n const taskId = String(rawTaskId || \"\").toLowerCase() === \"last\" ? getLatestLlmWorkerTaskIdV0() : rawTaskId;\n const snap = taskId ? getLlmWorkerTaskSnapshotV0(taskId) : null;\n if (!snap) {\n sendJson(res, 404, {\n ok: false,\n error: \"llm_task_not_found\",\n taskId: rawTaskId,\n status: \"missing\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n if (snap.status === \"processing\") {\n sendJson(res, 202, {\n ok: true,\n taskId: snap.taskId,\n status: \"processing\",\n createdAt: snap.createdAt,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n // Terminal states (completed/failed) always HTTP 200 — poll must not look like server fault (500).\n sendJson(res, 200, {\n ...(snap.body || { ok: false, error: \"llm_task_empty\", taskId: snap.taskId }),\n taskId: snap.taskId,\n status: snap.status === \"completed\" ? \"completed\" : snap.status,\n terminal: true,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n } catch (error) {\n console.error(\"[llm-task-poll] handler failed:\", error);\n sendJson(res, 200, {\n ok: false,\n error: \"llm_task_poll_handler_failed\",\n detail: String(error?.message || error),\n status: \"failed\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === rhizohRuntime.routes.rhizohLlm) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 128 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n logRhizohHealth(\"gateway_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: String(payload?.traceId || \"\"),\n auth: auth.ok ? \"ok\" : \"anon\",\n ip\n });\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (!checkHttpRateLimit(`rhizoh_llm:${rlKey}`, RL_RHIZOH_LLM_PER_MIN, 60_000)) {\n const rlTaxonomy = classifyStressResponseV0({ code: \"rate_limit_exceeded\" });\n return sendJson(res, 429, {\n ok: false,\n error: \"rate_limit_exceeded\",\n reply: rlTaxonomy.userMessageTr || \"İstek sınırı aşıldı. Kısa süre sonra tekrar deneyin.\",\n stressClass: rlTaxonomy.stressClass,\n responseAction: rlTaxonomy.responseAction,\n stressMatrix: rlTaxonomy.matrix,\n stressInterpretable: rlTaxonomy.interpretable,\n stressConfidence: rlTaxonomy.stressConfidence,\n actionConfidence: rlTaxonomy.actionConfidence,\n actionSoftened: rlTaxonomy.actionSoftened,\n responseActionStrict: rlTaxonomy.responseActionStrict,\n actionInterpretable: rlTaxonomy.actionInterpretable,\n conflictResolution: rlTaxonomy.conflictResolution,\n directive: \"NONE\"\n });\n }\n\n const requireExplicit = process.env.CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE === \"1\";\n const keyMode = normalizeClientLlmKeySource(payload?.llmKeySource ?? payload?.keySource);\n if (keyMode === null) {\n return sendJson(res, 400, {\n ok: false,\n error: \"invalid_llm_key_source\",\n reply: \"llmKeySource geçersiz. Kullanın: env | user_connection | auto\",\n directive: \"NONE\"\n });\n }\n if (requireExplicit && keyMode === \"auto\") {\n return sendJson(res, 400, {\n ok: false,\n error: \"llm_key_source_required\",\n reply: \"Sunucu CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE=1: istekte llmKeySource (env veya user_connection) zorunlu.\",\n directive: \"NONE\"\n });\n }\n\n const conn = auth.ok ? await resolveConnection(auth.uid, String(payload?.connectionId || \"\")) : null;\n\n if (keyMode === \"user_connection\") {\n if (!auth.ok) {\n return sendJson(res, 401, {\n ok: false,\n error: \"llm_auth_required_for_user_connection\",\n reply: \"Kayıtlı LLM bağlantısı için giriş gerekli.\",\n directive: \"NONE\"\n });\n }\n if (!conn?.apiKey) {\n return sendJson(res, 400, {\n ok: false,\n error: \"user_llm_connection_required\",\n reply: \"Geçerli bir LLM bağlantısı yok; Studio’da bağlantı ekleyin veya llmKeySource=env kullanın.\",\n directive: \"NONE\"\n });\n }\n }\n\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n let safePayload = applyLanguagePropagationToLlmPayloadV1({ ...(payload || {}) }, langProp);\n const trimOut = trimRhizohLlmGatewayPayloadV1(safePayload);\n if (trimOut.meta.trimmed) {\n logRhizohHealth(\"llm_payload_trimmed\", {\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n bytesBefore: trimOut.meta.bytesBefore,\n bytesAfter: trimOut.meta.bytesAfter\n });\n }\n safePayload = trimOut.payload;\n delete safePayload.apiKey;\n delete safePayload.llmKeySource;\n delete safePayload.keySource;\n\n const llmMessage = String(\n safePayload?.message || safePayload?.text || payload?.message || payload?.text || \"\"\n ).trim();\n if (!llmMessage) {\n return sendJson(res, 400, {\n ok: false,\n error: \"message_required\",\n reply: \"Mesaj metni gerekli (message alanı boş).\",\n directive: \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n if (!safePayload.message) {\n safePayload = { ...safePayload, message: llmMessage };\n }\n\n let resolvedProvider = payload?.provider;\n let resolvedModel = payload?.model;\n let connApiKey = \"\";\n\n ({\n resolvedProvider,\n resolvedModel,\n connApiKey\n } = resolveServerLlmProviderV0(keyMode, conn, payload));\n\n const turnInput = {\n safePayload,\n auth,\n keyMode,\n conn,\n resolvedProvider,\n resolvedModel,\n connApiKey\n };\n const turnMeta = {\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n langEcho: buildRhizohLanguagePropagationEchoV1(langProp)\n };\n\n if (\n isLlmWorkerEnabledV0() &&\n isLlmWorkerAsyncEnabledV0() &&\n String(req.headers[\"x-rhizoh-llm-sync\"] || \"\").trim() !== \"1\"\n ) {\n const taskId = enqueueLlmWorkerTaskAsyncV0(turnInput, turnMeta);\n logRhizohHealth(\"llm_worker_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n taskId,\n traceId: String(payload?.traceId || taskId),\n keyMode,\n async: true\n });\n sendJson(res, 202, {\n ok: true,\n taskId,\n status: \"processing\",\n traceId: String(payload?.traceId || taskId),\n pollPath: `/rhizoh/llm/task/${taskId}`,\n gatewayPid: process.pid,\n ...turnMeta.langEcho\n });\n return;\n }\n\n const { result, traceId, turnLatencyMs, spinePhases, sampledTrace } = isLlmWorkerEnabledV0()\n ? await runLlmWorkerTaskV0(turnInput, turnMeta)\n : await rhizohGatewayTurn(turnInput);\n logRhizohHealth(\"llm_response\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId,\n turnLatencyMs: Number(turnLatencyMs || 0),\n keyMode\n });\n sendJson(res, 200, {\n ...result,\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n traceId,\n turnLatencyMs,\n ...(spinePhases ? { spinePhases } : {}),\n ...(sampledTrace !== undefined ? { sampledTrace } : {})\n });\n } catch (error) {\n const msg = String(error?.message || \"\");\n const code = error?.code || \"\";\n let status = 500;\n if (msg === \"payload_too_large\") status = 413;\n else if (msg === \"rate_limit_exceeded\") status = 429;\n else if (\n code === \"cost_hard_limit\" ||\n code === \"phased_rollout_capacity\" ||\n error?.containment === true ||\n String(code).startsWith(\"agent_\")\n ) {\n status = 429;\n } else if (code === \"user_soft_blocked\" || code === \"prompt_abuse_detected\") status = 403;\n else if (\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(code) ||\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(msg) ||\n msg.includes(\"missing_api_key_for_\")\n )\n status = 400;\n let rhizohFailureKind = \"provider_error\";\n if (msg === \"rate_limit_exceeded\" || status === 429) rhizohFailureKind = \"rate_limit\";\n else if (status === 403) rhizohFailureKind = \"policy_block\";\n else if (status === 400 || status === 413) rhizohFailureKind = \"client_config\";\n else if (msg.startsWith(\"provider_http_\")) {\n const st = Number(msg.replace(\"provider_http_\", \"\"));\n if (st === 408 || st === 504) rhizohFailureKind = \"timeout\";\n else rhizohFailureKind = \"provider_error\";\n } else if (/timeout|timed out|ETIMEDOUT/i.test(msg)) rhizohFailureKind = \"timeout\";\n\n const providerHttpMatch = msg.match(/^provider_http_(\\d{3})$/);\n const stressTaxonomy =\n error?.stressClass != null\n ? {\n stressClass: error.stressClass,\n responseAction: error.responseAction,\n stressMatrix: error.stressMatrix,\n stressInterpretable: error.stressInterpretable !== false,\n stressConfidence: error.stressConfidence,\n actionConfidence: error.actionConfidence,\n actionSoftened: error.actionSoftened,\n responseActionStrict: error.responseActionStrict,\n actionInterpretable: error.actionInterpretable,\n conflictResolution: error.conflictResolution,\n stressSecondary: error.stressSecondary,\n responseActions: error.responseActions\n }\n : (() => {\n const t = classifyStressResponseV0({\n code: code || msg,\n rhizohFailureKind,\n providerHttpStatus: providerHttpMatch\n ? Number(providerHttpMatch[1])\n : null\n });\n return {\n stressClass: t.stressClass,\n responseAction: t.responseAction,\n stressMatrix: t.matrix,\n stressInterpretable: t.interpretable,\n stressConfidence: t.stressConfidence,\n actionConfidence: t.actionConfidence,\n actionSoftened: t.actionSoftened,\n responseActionStrict: t.responseActionStrict,\n actionInterpretable: t.actionInterpretable,\n conflictResolution: t.conflictResolution,\n stressSecondary: t.stressSecondary,\n responseActions: t.responseActions\n };\n })();\n sendJson(res, status, {\n ok: false,\n rhizohFailureKind,\n ...(providerHttpMatch ? { providerHttpStatus: Number(providerHttpMatch[1]) } : {}),\n error: code || msg || \"rhizoh_llm_failed\",\n detail: msg || code || undefined,\n reply:\n error?.reply ||\n (status === 500 ? \"Rhizoh bağlantısı geçici olarak kesildi.\" : msg),\n directive: error?.directive || \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n ...stressTaxonomy\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicSeal) {\n try {\n if (!EPISTEMIC_SEAL_SECRET) {\n sendJson(res, 503, { ok: false, error: \"epistemic_seal_disabled\" });\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`epistemic_seal:${ip}`, RL_EPISTEMIC_SEAL_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const truth = body.truth_contract;\n if (!truth || typeof truth !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"truth_contract_required\" });\n return;\n }\n const sealBody = {\n truth_contract: truth,\n runtime_hash: String(body.runtime_hash || \"\"),\n model_route: {\n provider: body.model_route?.provider ?? null,\n model: body.model_route?.model ?? null\n },\n memory_digest: String(body.memory_digest || \"\"),\n world_snapshot_hash: String(body.world_snapshot_hash || \"\"),\n timestamp: Number(body.timestamp) || Date.now()\n };\n const canonical = canonicalEpistemicSealString(sealBody);\n const { hash, signature } = hashAndSignEpistemicSeal(canonical, EPISTEMIC_SEAL_SECRET);\n recordGenesisEpistemicSealIssued(hash);\n const attestation = {\n schema: EPISTEMIC_SEAL_SCHEMA,\n algorithm: \"SHA-256+HMAC-SHA256\",\n keyHint: process.env.CASTLE_EPISTEMIC_SEAL_SECRET ? \"CASTLE_EPISTEMIC_SEAL_SECRET\" : \"CASTLE_GATEWAY_TOKEN\",\n issuedAt: Date.now(),\n gateway: { service: \"castle-gateway\", listenPort: PORT }\n };\n sendJson(res, 200, {\n ok: true,\n hash,\n signature,\n attestation,\n canonicalBytes: Buffer.byteLength(canonical, \"utf8\")\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"seal_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicLogsBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`epistemic_logs:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 120) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const persisted = await persistEpistemicLedgerBatch(auth.uid, entries);\n recordGenesisEpistemicLedgerPersisted(persisted.persisted);\n // Internal cognition layer: forecast stays gateway/ledger-only (no client exposure).\n try {\n await persistEpistemicForecastBatch(auth.uid, persisted.normalized || []);\n } catch {\n /* forecast failures must not break primary ledger ingest */\n }\n sendJson(res, 200, {\n ok: true,\n persisted: persisted.persisted,\n mode: persisted.mode,\n latest: persisted.normalized?.[persisted.normalized.length - 1] || null\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"epistemic_log_ingest_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.authorityLedgerBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`authority_ledger:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 80) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const witnessed = persistAuthorityLedgerWitnessBatchV1(auth.uid, entries, EPISTEMIC_SEAL_SECRET);\n if (witnessed.witnessed > 0) {\n recordGenesisEpistemicLedgerPersisted(witnessed.witnessed);\n const clientSeal = witnessed.lastWitness?.clientSealHash;\n if (clientSeal) recordGenesisEpistemicSealIssued(clientSeal);\n }\n sendJson(res, 200, {\n ok: true,\n witnessed: witnessed.witnessed,\n quarantined: witnessed.quarantined,\n chainHeight: witnessed.chainHeight,\n chainHead: witnessed.chainHead,\n lastWitness: witnessed.lastWitness,\n results: witnessed.results\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"authority_ledger_witness_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.authorityLedgerReplay) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const replayUrl = new URL(String(req.url || \"\"), \"http://localhost\");\n const epochId = String(replayUrl.searchParams.get(\"epochId\") || \"\").trim() || undefined;\n const snapshot = getAuthorityLedgerWitnessSnapshotV1(auth.uid, epochId);\n const replay = replayAuthorityLedgerWitnessChainV1(auth.uid, epochId);\n sendJson(res, 200, {\n ok: true,\n snapshot,\n replay,\n schema: \"castle.rhizoh.authority_ledger_witness.v1.replay_surface\"\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"authority_ledger_replay_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.authorityEpochMerge) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`authority_epoch_merge:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const merged = assimilateAuthorityEpochMergeV1(auth.uid, body);\n if (!merged.ok) {\n sendJson(res, 400, merged);\n return;\n }\n sendJson(res, 200, {\n ok: true,\n mergeEvent: merged.mergeEvent,\n recentMerges: merged.recentMerges\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"authority_epoch_merge_failed\") });\n }\n return;\n }\n\n sendJson(res, 404, { ok: false, error: \"not_found\" });\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\nspiralWssForGenesis = wss;\nconst orchestrator = createOrchestrator();\nconst clientStats = new Map();\nlet broadcasterClientId = null;\nconst spiralState = {\n rooms: [{ id: \"room-default\", name: \"Spiral Default Arena\" }],\n activeByClient: new Map(),\n characters: new Map() // id -> {id,name,role,roomId,source}\n};\n\n/** Castle social rooms — keyed by `castleRoomKey` from {@link WS_MESSAGE.CASTLE_SOCIAL_PULSE}. */\nconst castleSocialRoomState = new Map();\nconst CASTLE_SOCIAL_MEMBER_TTL_MS = 90_000;\nconst CASTLE_SOCIAL_MAX_PULSE_BYTES = 8192;\nconst RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES = 5 * 1024 * 1024;\nconst RHIZOH_VOICE_LIVE_MAX_CHUNKS = 96;\n\n/**\n * Voice Engine v3 live media lane.\n * Client streams MediaRecorder chunks over the existing gateway WS; STOP reuses the\n * gateway ASR runner so orchestration remains the single authority downstream.\n */\nconst rhizohVoiceLiveSessions = new WeakMap();\n\nfunction appendRhizohVoiceLiveEventV0(session, type, payloadRefSeed = \"\") {\n if (!session) return null;\n session.eventSeq = (Number(session.eventSeq) || 0) + 1;\n const event = createRhizohImmutableEventV0({\n type,\n sessionId: session.sessionId,\n traceId: session.traceId,\n actorSeed: session.sessionId || session.traceId,\n eventSeq: session.eventSeq,\n payloadRef: createRhizohPayloadRefV0(payloadRefSeed || `${session.sessionId}:${type}:${session.eventSeq}`)\n });\n session.events = [...(session.events || []), event].slice(-64);\n return event;\n}\n\nfunction sendRhizohVoiceLiveEnvelopeV0(socket, type, payload) {\n try {\n if (socket?.readyState === 1) socket.send(JSON.stringify(createEnvelope(type, payload)));\n } catch {\n /* noop */\n }\n}\n\nfunction decodeRhizohVoiceLiveChunkV0(payload = {}) {\n const b64 = String(payload.audioBase64 || \"\").trim();\n if (!b64) return { ok: false, error: \"audio_base64_required\" };\n try {\n const buf = Buffer.from(b64, \"base64\");\n if (!buf.length) return { ok: false, error: \"audio_decode_empty\" };\n return { ok: true, buffer: buf };\n } catch {\n return { ok: false, error: \"audio_base64_invalid\" };\n }\n}\n\nfunction handleRhizohVoiceLiveStartV0(socket, payload = {}) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const sessionId = String(p.sessionId || `live_${Date.now().toString(36)}`).slice(0, 96);\n rhizohVoiceLiveSessions.set(socket, {\n sessionId,\n traceId: String(p.traceId || \"\").slice(0, 128),\n mimeType: String(p.mimeType || \"audio/webm\").slice(0, 80),\n languageCode: String(p.languageCode || \"tr-TR\").slice(0, 32),\n path: String(p.path || \"both\").slice(0, 24),\n chunks: [],\n chunkIndexes: [],\n totalBytes: 0,\n nextChunkIndex: 1,\n eventSeq: 0,\n events: [],\n startedAtMs: Date.now()\n });\n appendRhizohVoiceLiveEventV0(rhizohVoiceLiveSessions.get(socket), \"VOICE_LIVE_SESSION_STARTED\", `${sessionId}:live_session`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_START, {\n ok: true,\n sessionId,\n schema: \"castle.rhizoh.voice_live.gateway_lane.v0\"\n });\n}\n\nfunction handleRhizohVoiceLiveChunkV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const sequence = validateRhizohVoiceLiveChunkPayloadV0(payload, {\n expectedChunkIndex: session.nextChunkIndex\n });\n if (!sequence.ok) {\n rhizohVoiceLiveSessions.delete(socket);\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:chunk_reject`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: sequence.error,\n expectedChunkIndex: sequence.expectedChunkIndex,\n chunkIndex: sequence.chunkIndex\n });\n return;\n }\n const decoded = decodeRhizohVoiceLiveChunkV0(payload);\n if (!decoded.ok) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: decoded.error\n });\n return;\n }\n if (\n session.chunks.length >= RHIZOH_VOICE_LIVE_MAX_CHUNKS ||\n session.totalBytes + decoded.buffer.length > RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES\n ) {\n rhizohVoiceLiveSessions.delete(socket);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_audio_too_large\"\n });\n return;\n }\n session.chunks.push(decoded.buffer);\n session.chunkIndexes.push(sequence.chunkIndex);\n session.totalBytes += decoded.buffer.length;\n session.nextChunkIndex += 1;\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_CHUNK_ACCEPTED\", `${session.sessionId}:chunk:${sequence.chunkIndex}`);\n}\n\nfunction handleRhizohVoiceLiveStopV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n rhizohVoiceLiveSessions.delete(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const lastChunkIndex = Number(payload?.lastChunkIndex);\n if (Number.isInteger(lastChunkIndex) && lastChunkIndex !== session.chunks.length) {\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:stop_mismatch`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_chunk_count_mismatch\",\n expectedChunkCount: lastChunkIndex,\n receivedChunkCount: session.chunks.length\n });\n return;\n }\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_STOP_RECEIVED\", `${session.sessionId}:stop:${session.chunks.length}`);\n const audio = Buffer.concat(session.chunks);\n if (!audio.length) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"audio_empty\"\n });\n return;\n }\n runRhizohVoiceTranscribeV3({\n path: String(payload?.path || session.path || \"both\"),\n audioBase64: audio.toString(\"base64\"),\n mimeType: String(payload?.mimeType || session.mimeType || \"audio/webm\"),\n languageCode: String(payload?.languageCode || session.languageCode || \"tr-TR\"),\n traceId: String(payload?.traceId || session.traceId || \"\"),\n sessionId: session.sessionId,\n transport: \"gateway_ws_live\"\n })\n .then((result) => {\n appendRhizohVoiceLiveEventV0(session, result?.ok ? \"VOICE_TRANSCRIBE_FINAL\" : \"VOICE_TRANSCRIBE_FAILED\", `${session.sessionId}:final`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_FINAL, {\n ...result,\n sessionId: session.sessionId,\n transportPath: \"gateway_ws_live\",\n bytes: audio.length,\n chunkCount: session.chunks.length,\n chunkSequence: Object.freeze({\n ok: true,\n firstChunkIndex: session.chunkIndexes[0] || null,\n lastChunkIndex: session.chunkIndexes[session.chunkIndexes.length - 1] || null,\n receivedChunkCount: session.chunks.length\n }),\n eventTrace: Object.freeze((session.events || []).slice(-16)),\n recordedMs: Math.max(0, Date.now() - session.startedAtMs)\n });\n })\n .catch((e) => {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_transcribe_failed\",\n detail: String(e?.message || e)\n });\n });\n}\n\nfunction removeCastleSocialClientFromAllRooms(clientId) {\n const cid = String(clientId || \"\");\n if (!cid) return;\n for (const room of castleSocialRoomState.values()) {\n room.members.delete(cid);\n }\n}\n\nfunction buildCastleSocialRoomPayload(roomKey) {\n const room = castleSocialRoomState.get(roomKey);\n if (!room) return null;\n const now = Date.now();\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const roster = [...room.members.values()].map((m) => ({\n userId: m.userId,\n lastMs: m.lastMs,\n ...m.sp\n }));\n const peerFeeds =\n room.walPeers && room.walPeers.size\n ? [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }))\n : [];\n return { castleRoomKey: roomKey, seq: room.seq, roster, peerFeeds, ts: now };\n}\n\nconst CASTLE_WAL_PEER_MAX_FEED_BYTES = 48 * 1024;\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {{ ok: boolean, code?: string, reason?: string }} gate\n */\nfunction rejectWalPeerIngressV0(socket, gate) {\n const code = gate.code || \"wal_peer_rejected\";\n recordWalPeerRejectV0(code);\n recordSubstrateOtelEventV0(\"wal_peer_reject\", { code, reason: String(gate.reason || \"\").slice(0, 120) });\n socket.send(\n JSON.stringify(\n createEnvelope(WS_MESSAGE.ERROR, {\n error: gate.reason || \"WAL peer ingress rejected.\",\n code\n })\n )\n );\n}\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {Record<string, unknown> | null | undefined} walPeerFeed\n * @returns {boolean}\n */\nfunction gateWalPeerIngressV0(socket, walPeerFeed) {\n const authGate = assertWalPeerSocketAuthorityV0(socket);\n if (!authGate.ok) {\n rejectWalPeerIngressV0(socket, authGate);\n return false;\n }\n const sigGate = validateWalPeerFeedSignatureV0(\n walPeerFeed && typeof walPeerFeed === \"object\" ? walPeerFeed : {}\n );\n if (!sigGate.ok) {\n rejectWalPeerIngressV0(socket, sigGate);\n return false;\n }\n return true;\n}\n\nfunction handleCastleWalPeerFeed(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_WAL_PEER_MAX_FEED_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_WAL_PEER_FEED too large.\" })));\n return;\n }\n const walPeerFeed = p.walPeerFeed && typeof p.walPeerFeed === \"object\" ? p.walPeerFeed : {};\n if (!gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const castleRoomKey = String(p.castleRoomKey || \"wal:convergence\").slice(0, 64);\n const castleId = String(p.castleId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId,\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: room.members.get(socket.clientId)?.sp ?? {}\n });\n const peerFeeds = [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }));\n const body = {\n castleRoomKey,\n seq: room.seq,\n peerFeeds,\n ts: now\n };\n const env = createEnvelope(WS_MESSAGE.CASTLE_WAL_PEER_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys(), ...room.walPeers.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction handleCastleSocialPulse(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_SOCIAL_MAX_PULSE_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_SOCIAL_PULSE too large.\" })));\n return;\n }\n const castleRoomKey = String(p.castleRoomKey || \"default\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n const sp = p.socialPulse && typeof p.socialPulse === \"object\" ? p.socialPulse : {};\n const walPeerFeed =\n p.walPeerFeed && typeof p.walPeerFeed === \"object\"\n ? p.walPeerFeed\n : sp.walPeerFeed && typeof sp.walPeerFeed === \"object\"\n ? sp.walPeerFeed\n : null;\n if (walPeerFeed && !gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const spNext = walPeerFeed ? { ...sp, walPeerFeed } : { ...sp };\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: spNext\n });\n if (walPeerFeed) {\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId: String(p.castleId || userId).slice(0, 64),\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n }\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const body = buildCastleSocialRoomPayload(castleRoomKey);\n if (!body) return;\n const env = createEnvelope(WS_MESSAGE.CASTLE_SOCIAL_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcast(msgObj) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcastState() {\n broadcast(\n createEnvelope(WS_MESSAGE.BROADCAST_STATE, {\n broadcasterClientId,\n viewers: [...wss.clients].filter((c) => c.readyState === 1).length\n })\n );\n}\n\nfunction currentSpiralStatePayload() {\n return {\n rooms: spiralState.rooms,\n characters: [...spiralState.characters.values()]\n };\n}\n\nfunction broadcastSpiralState() {\n broadcast(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload()));\n}\n\nfunction ensureRoom(roomId, fallbackName = null) {\n if (!roomId) return spiralState.rooms[0].id;\n const exists = spiralState.rooms.find((r) => r.id === roomId);\n if (exists) return exists.id;\n const created = { id: roomId, name: fallbackName || `Spiral-${roomId.slice(-4)}` };\n spiralState.rooms.push(created);\n return created.id;\n}\n\nfunction syncSpiralCharactersWithWorld(snapshot) {\n const agents = snapshot?.agents || [];\n for (const a of agents) {\n if (!spiralState.characters.has(a.id)) {\n spiralState.characters.set(a.id, {\n id: a.id,\n name: a.id,\n role: \"agent\",\n roomId: spiralState.rooms[0].id,\n source: \"world\"\n });\n }\n }\n}\n\nfunction broadcastExcept(msgObj, exceptSocket) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client !== exceptSocket && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction validateInputFrame(payload, socket) {\n const stats = clientStats.get(socket.clientId) || { lastFrame: 0, windowStart: Date.now(), msgCount: 0 };\n const now = Date.now();\n if (now - stats.windowStart > 5000) {\n stats.windowStart = now;\n stats.msgCount = 0;\n }\n stats.msgCount += 1;\n clientStats.set(socket.clientId, stats);\n if (stats.msgCount > 120) return \"Rate limit exceeded.\";\n\n if (!payload || !Number.isInteger(payload.frame)) return \"frame must be integer.\";\n if (payload.frame <= stats.lastFrame) return \"frame must be monotonic.\";\n const commands = Array.isArray(payload.commands) ? payload.commands : [];\n if (commands.length > 8) return \"Too many commands in one frame.\";\n for (const cmd of commands) {\n if (!cmd || typeof cmd.command !== \"string\") return \"Invalid command format.\";\n if (\n cmd.command !== \"SPAWN_AGENT\" &&\n cmd.command !== \"LIST_CASTLES\" &&\n cmd.command !== \"SPAWN_ENTITY\" &&\n cmd.command !== \"CREATE_CASTLE\"\n ) return \"Unsupported command.\";\n }\n stats.lastFrame = payload.frame;\n clientStats.set(socket.clientId, stats);\n return null;\n}\n\nfunction validateSignalPayload(payload) {\n if (!payload || typeof payload !== \"object\") return \"Invalid signaling payload.\";\n const signalType = payload.signalType;\n if (Object.values(CASTLE_NETWORK_SIGNAL_V0).includes(signalType)) {\n return null;\n }\n if (![\"OFFER\", \"ANSWER\", \"ICE\"].includes(signalType)) return \"Invalid signal type.\";\n if (typeof payload.to !== \"string\" || payload.to.length < 3) return \"Invalid signal destination.\";\n if (signalType !== \"ICE\" && JSON.stringify(payload.sdp || {}).length > 16 * 1024) return \"SDP too large.\";\n if (signalType === \"ICE\" && JSON.stringify(payload.candidate || {}).length > 6 * 1024) return \"ICE too large.\";\n return null;\n}\n\nfunction sanitizeRawMessage(raw) {\n if (!raw) return null;\n const text = raw.toString();\n if (Buffer.byteLength(text, \"utf8\") > MAX_MESSAGE_BYTES) return null;\n return text;\n}\n\nwss.on(\"connection\", async (socket, req) => {\n const origin = req?.headers?.origin || \"\";\n if (!httpCors.isWebSocketOriginAllowed(origin)) {\n socket.close(1008, \"Origin not allowed\");\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const path = req?.url || \"\";\n const query = new URL(`ws://localhost${path}`).searchParams;\n const token = query.get(\"token\");\n if (!token || token !== REQUIRED_GATEWAY_TOKEN) {\n socket.close(1008, \"Invalid gateway token\");\n return;\n }\n }\n if (REQUIRE_AUTH) {\n const result = await verifyClientToken(req);\n if (!result.ok) {\n const isLocal = origin.includes(\"localhost\") || origin.includes(\"127.0.0.1\") || !origin;\n if (!(ALLOW_DEV_ANON && isLocal)) {\n socket.close(1008, result.reason);\n return;\n }\n socket.auth = { ok: true, kind: \"dev-anon\", user: { uid: \"dev-anon\" } };\n } else {\n socket.auth = result;\n }\n }\n\n socket.clientId = `c-${Math.random().toString(36).slice(2, 8)}`;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.HELLO, { message: \"Castle Gateway online.\", clientId: socket.clientId })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.WORLD_SNAPSHOT, orchestrator.worldSnapshot())));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.BROADCAST_STATE, { broadcasterClientId, viewers: [...wss.clients].filter((c) => c.readyState === 1).length })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload())));\n broadcastExcept(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }), socket);\n\n socket.on(\"message\", (raw) => {\n const safeRaw = sanitizeRawMessage(raw);\n if (!safeRaw) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Message too large or invalid.\" })));\n return;\n }\n const parsed = safeJsonParse(safeRaw);\n if (!parsed?.type) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid message.\" })));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND_TEXT) {\n const canonical = parseTextToCommand(parsed.payload?.text);\n if (!canonical) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Unknown command text.\" })));\n return;\n }\n const result = orchestrator.applyCommand(canonical);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.MATCH_SESSION_JOIN) {\n handleMatchSessionJoinV0(socket, parsed, wss);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.MATCH_CASTLE_INVITE) {\n handleMatchCastleInviteV0(socket, parsed, wss);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.MATCH_MOVE) {\n handleMatchMoveAuthorityV0(socket, parsed, wss);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND) {\n const result = orchestrator.applyCommand(parsed.payload);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n }\n\n if (parsed.type === WS_MESSAGE.INPUT_FRAME) {\n const err = validateInputFrame(parsed.payload, socket);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const frame = parsed.payload.frame;\n const commands = Array.isArray(parsed.payload?.commands) ? parsed.payload.commands : [];\n orchestrator.queueInputFrame(socket.clientId, frame, commands);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, queued: true, frame })));\n }\n\n if (parsed.type === WS_MESSAGE.SIGNAL) {\n const payload = parsed.payload || {};\n if (Object.values(CASTLE_NETWORK_SIGNAL_V0).includes(payload.signalType)) {\n const netErr = validateCastleNetworkSignalPayloadV0(socket, payload);\n if (netErr) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: netErr })));\n return;\n }\n handleCastleNetworkSignalV0(socket, payload, wss);\n return;\n }\n const err = validateSignalPayload(payload);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const signalType = payload.signalType;\n const to = payload.to;\n for (const client of wss.clients) {\n if (client.clientId === to && client.readyState === 1) {\n client.send(JSON.stringify(createEnvelope(WS_MESSAGE.SIGNAL, { ...payload, from: socket.clientId })));\n break;\n }\n }\n }\n\n if (parsed.type === WS_MESSAGE.DRONE_TELEMETRY) {\n const payload = parsed.payload || {};\n const json = JSON.stringify(payload);\n if (Buffer.byteLength(json, \"utf8\") > 48 * 1024) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"DRONE_TELEMETRY payload too large.\" })));\n return;\n }\n broadcastExcept(createEnvelope(WS_MESSAGE.DRONE_TELEMETRY, { ...payload, relayFrom: socket.clientId }), socket);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_START) {\n handleRhizohVoiceLiveStartV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_CHUNK) {\n handleRhizohVoiceLiveChunkV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_STOP) {\n handleRhizohVoiceLiveStopV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_SOCIAL_PULSE) {\n handleCastleSocialPulse(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_WAL_PEER_FEED) {\n handleCastleWalPeerFeed(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.OPEN_DATA_QUERY) {\n queryOpenData(parsed.payload)\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: true, result })));\n })\n .catch((e) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: false, error: e.message })));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_QUERY) {\n runRhizohBrainV2({\n ...(parsed.payload || {}),\n uid: socket?.auth?.user?.uid || \"unknown\"\n })\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, result)));\n })\n .catch(async () => {\n const fallback = await runRhizohBrain(parsed.payload || {}).catch(() => ({\n ok: false,\n reply: \"Rhizoh temporary disturbance. Habitat remains stable.\"\n }));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, fallback)));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.BROADCAST_REGISTER) {\n const role = String(parsed.payload?.role || \"\");\n if (role === \"GENESIS_BROADCAST_AGENT\") {\n broadcasterClientId = socket.clientId;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, registered: role })));\n broadcastState();\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.STUDIO_CUE) {\n // Only registered broadcaster can push global cues.\n if (!broadcasterClientId || socket.clientId !== broadcasterClientId) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Only broadcaster agent can emit STUDIO_CUE.\" })));\n return;\n }\n const cue = parsed.payload || {};\n if (typeof cue !== \"object\") {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid STUDIO_CUE payload.\" })));\n return;\n }\n broadcast(createEnvelope(WS_MESSAGE.STUDIO_CUE, cue));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_JOIN_ROOM) {\n const requested = String(parsed.payload?.roomId || \"\").trim();\n const requestedName = String(parsed.payload?.roomName || \"\").trim();\n const roomId = ensureRoom(requested || `room-${Math.random().toString(36).slice(2, 7)}`, requestedName || null);\n spiralState.activeByClient.set(socket.clientId, roomId);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, roomId })));\n broadcastSpiralState();\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_CREATE_CHARACTER) {\n const name = String(parsed.payload?.name || \"\").trim().slice(0, 32) || `AI-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;\n const role = String(parsed.payload?.role || \"agent\").trim().slice(0, 24) || \"agent\";\n const roomId = ensureRoom(\n String(parsed.payload?.roomId || spiralState.activeByClient.get(socket.clientId) || spiralState.rooms[0].id),\n null\n );\n\n // Character creation always maps to an authoritative world agent.\n const result = orchestrator.applyCommand({\n command: COMMAND.SPAWN_AGENT,\n params: { targetCastleId: \"castle-01\" }\n });\n if (!result?.ok || !result?.data?.id) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Character creation failed.\" })));\n return;\n }\n const id = result.data.id;\n spiralState.characters.set(id, { id, name, role, roomId, source: \"spawned\" });\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, characterId: id, roomId })));\n broadcastSpiralState();\n return;\n }\n });\n\n socket.on(\"close\", () => {\n if (socket.clientId === broadcasterClientId) broadcasterClientId = null;\n leaveMatchBroadcastRoomV0(socket);\n clientStats.delete(socket.clientId);\n rhizohVoiceLiveSessions.delete(socket);\n spiralState.activeByClient.delete(socket.clientId);\n removeCastleSocialClientFromAllRooms(socket.clientId);\n removeCastleNetworkClientV0(socket.clientId, wss);\n broadcast(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }));\n broadcastState();\n });\n});\n\nsetInterval(() => {\n void (async () => {\n try {\n const { frame, snapshot, delta, packedDelta } = orchestrator.tick(1 / 20);\n syncSpiralCharactersWithWorld(snapshot);\n broadcast(createEnvelope(WS_MESSAGE.NETWORK_TICK, { frame }));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_TICK, snapshot));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA, delta));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA_PACKED, packedDelta));\n broadcastSpiralState();\n await runAcademyEventTick();\n const executed = await tickAutomations(Date.now());\n for (const a of executed) {\n await addTranscript(a.uid, {\n source: \"planner\",\n eventType: \"automation-executed\",\n text: `Automation executed: ${a.title} (${a.action})`,\n roomId: \"studio-main\",\n meta: { automationId: a.id, action: a.action, deviceId: a.deviceId || \"\" }\n });\n }\n } catch (e) {\n console.error(\"[GATEWAY] world tick / automations loop error:\", String(e?.message || e));\n }\n })();\n}, 50);\n\n(async () => {\n await rhizohRuntime.telemetry.initOpenTelemetry();\n if (String(process.env.CASTLE_GENESIS_DISK_PERSIST ?? \"\").trim() === \"\") {\n process.env.CASTLE_GENESIS_DISK_PERSIST = \"1\";\n }\n const genesisDiskBoot = await hydrateGenesisContinuityPersistenceBootV0();\n if (!genesisDiskBoot.ok) {\n console.warn(\"[GATEWAY] genesis disk hydrate failed; continuing with in-memory genesis continuity\");\n }\n httpServer.listen(PORT, () => {\n startGenesisCanonicalClock();\n installGenesisCheckpointSurfaceGetter(buildGenesisRuntimeSurfacePayloadLive);\n setGenesisContinuityAfterPublishHook((seq) => {\n noteGenesisCheckpointSeqCommitted(seq);\n noteGenesisContinuitySeqAcceptedV0(seq);\n });\n startGenesisContinuityInfraSampler(buildGenesisRuntimeSurfacePayloadLive, 2000);\n logProductionObservatorySurfaceGuardsV0();\n logGatewaySubstrateAuthorityGuardsV0();\n if (isLlmWorkerEnabledV0()) {\n try {\n const workerBoot = initLlmWorkerAtBootV0();\n console.log(\"[GATEWAY] llm worker boot:\", workerBoot);\n } catch (workerErr) {\n console.error(\"[GATEWAY] llm worker boot failed:\", workerErr);\n }\n }\n console.log(`[GATEWAY] ws/http://localhost:${PORT}`);\n console.log(\n `[GATEWAY] genesis continuity: GET ${rhizohRuntime.routes.genesisRuntime} | POST ${rhizohRuntime.routes.genesisIngress} | SSE ${rhizohRuntime.routes.genesisStream} | replay ${rhizohRuntime.routes.genesisReplay} | diff ${rhizohRuntime.routes.genesisReplayDiff} | equiv ${rhizohRuntime.routes.genesisReplayEquivalence} | analytics ${rhizohRuntime.routes.genesisReplayAnalytics} | evolution ${rhizohRuntime.routes.genesisReplayEvolution}`\n );\n if (String(process.env.CASTLE_GENESIS_OBSERVATORY_LIVE_V01 ?? \"\").trim() === \"1\") {\n console.log(\"GENESIS OBSERVATORY LIVE v0.1 — READ ONLY PHASE STARTED\");\n }\n });\n})().catch((e) => {\n console.error(\"[GATEWAY] fatal boot (before or during listen):\", e);\n process.exit(1);\n});\n"}, "450f43628d3e76277f7e618e01edf2e7ccdffde7": {"t": 1785754122.4352393, "n": 177226, "h": "fb27da377b590ad6", "c": "import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport dotenv from \"dotenv\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconsole.log(\"🔥 [GENESIS_BOOT] entry file:\", __filename);\nconsole.log(\"🔥 [GENESIS_BOOT] node env:\", process.env.NODE_ENV);\nconsole.log(\"🔥 [GENESIS_BOOT] pid:\", process.pid);\nconsole.log(\"🧬 [GENESIS_BOOT] cwd:\", process.cwd());\nconsole.log(\"🧬 [GENESIS_BOOT] import.meta.url:\", import.meta.url);\ndotenv.config({ path: path.join(__dirname, \"..\", \".env\") });\ndotenv.config({ path: path.join(__dirname, \"..\", \".env.local\"), override: true });\n\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport { createServer } from \"node:http\";\nimport { createOrchestrator } from \"@castle/orchestrator\";\nimport { parseTextToCommand } from \"@castle/command-dsl\";\nimport {\n WS_MESSAGE,\n COMMAND,\n createEnvelope,\n createRhizohImmutableEventV0,\n createRhizohPayloadRefV0,\n safeJsonParse,\n validateRhizohVoiceLiveChunkPayloadV0\n} from \"@castle/protocol\";\nimport { queryOpenData } from \"./openData.js\";\nimport { verifyClientToken } from \"./auth.js\";\nimport { handleMatchMoveAuthorityV0, leaveMatchBroadcastRoomV0 } from \"./rhizoh/matchMoveAuthorityV0.js\";\nimport { handleMatchSessionJoinV0 } from \"./rhizoh/matchBroadcastRoomV0.js\";\nimport { queryRhizohLlm } from \"./rhizohLlmGateway.js\";\nimport { rhizohGatewayTurn } from \"./rhizohGatewayTurn.js\";\nimport {\n enqueueLlmWorkerTaskAsyncV0,\n getLatestLlmWorkerTaskIdV0,\n getLlmWorkerDebugSnapshotV0,\n getLlmWorkerTaskSnapshotV0,\n initLlmWorkerAtBootV0,\n isLlmWorkerAsyncEnabledV0,\n isLlmWorkerEnabledV0,\n runLlmWorkerTaskV0\n} from \"./llmWorkerRuntime.js\";\nimport {\n RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n runRhizohVoiceTranscribeV3,\n rhizohVoiceTranscribeEnvV3\n} from \"./rhizohVoiceTranscribeV3.js\";\nimport { createHttpCorsPolicy, normalizeHttpOrigin, parseAllowedOriginsFromEnv } from \"./httpCorsPolicyV1.js\";\nimport {\n applyLanguagePropagationToLlmPayloadV1,\n applyLanguagePropagationToVoiceBodyV1,\n buildRhizohLanguagePropagationEchoV1,\n parseRhizohLanguagePropagationV1\n} from \"./rhizohLanguagePropagationV1.js\";\nimport { trimRhizohLlmGatewayPayloadV1 } from \"./rhizohLlmPayloadTrimV1.js\";\nimport {\n meshAppendDelta,\n meshContinuityAggregate,\n meshJoin,\n meshLeave,\n meshReplayFromNodeId,\n meshReplayFromSeq,\n meshSnapshot,\n meshSubscribeSse\n} from \"./presenceRoomMesh.js\";\nimport { runRhizohBrainV2 } from \"./rhizohBrainV2.js\";\nimport {\n listConnections,\n createConnection,\n updateConnection,\n deleteConnection,\n setDefaultConnection,\n resolveConnection\n} from \"./llmConnectionsStore.js\";\nimport { getCloudSyncSnapshotV0, mergeCloudSyncSnapshotV0 } from \"./castleCloudSyncStore.js\";\nimport {\n CASTLE_NETWORK_SIGNAL_V0,\n handleCastleNetworkSignalV0,\n listCastleNetworkPresenceV0,\n removeCastleNetworkClientV0,\n validateCastleNetworkSignalPayloadV0\n} from \"./castleNetworkRelayV0.js\";\nimport { checkHttpRateLimit, getHttpClientIp } from \"./castleHttpRateLimit.js\";\nimport {\n submitAbuseReportV0,\n listModerationQueueV0,\n softBlockUserV0,\n removeSoftBlockV0,\n listSoftBlockedUsersV0\n} from \"./ops/moderationMvpV0.js\";\nimport { listRecentAgentSnapshotsV0 } from \"./ops/agentObservabilityV0.js\";\nimport { readAgentContainmentConfigV0 } from \"./ops/agentContainmentV0.js\";\nimport { readCostContainmentConfigV0, getCostContainmentStatsV0 } from \"./ops/costContainmentV0.js\";\nimport { getGlobalCostLedgerSnapshotV0, getGclBackendStatusV0 } from \"./ops/globalCostLedgerV0.js\";\nimport { evaluateEconomicStrategyL3V0 } from \"./ops/economicStrategyEngineL3V0.js\";\nimport { getPhasedRolloutStatsV0, readPhasedRolloutTierV0 } from \"./ops/phasedRolloutV0.js\";\nimport { buildUnifiedStateNarrativeV0 } from \"./ops/unifiedStateNarrativeV0.js\";\nimport { resolveHardeningTenantIdV0 } from \"./ops/narrativeTenantIsolationV0.js\";\nimport {\n classifyStressResponseV0,\n verifyStressTaxonomyCoverageV0,\n verifyStressConflictResolutionV0\n} from \"./ops/stressResponseTaxonomyV0.js\";\nimport { verifyResolutionStabilityEnvelopeV0 } from \"./ops/resolutionStabilityEnvelopeV0.js\";\nimport { logLlmAccess } from \"./castleLlmAudit.js\";\nimport { appendMemory, listMemories, getMemoryContext, getPersonaGoalMemory, setPersonaGoalMemory, autoCompactMemories } from \"./memoryStore.js\";\nimport { getFirebasePersistence } from \"./firebasePersistence.js\";\nimport { registerAgentIdentity, listAgentIdentities, getAgentIdentity, updateAgentIdentity } from \"./agentIdentityStore.js\";\nimport { queueAcademyEvent, listAcademyEvents, resolveAcademyEvent, runAcademyEventTick } from \"./academyEventEngine.js\";\nimport { enqueuePdfJob, getPdfJob } from \"./pdfPipeline.js\";\nimport {\n createPublishSession,\n listPublishSessions,\n updatePublishSession,\n addTranscript,\n listTranscripts,\n searchMetadata,\n registerDevice,\n listDevices,\n updateDevice,\n appendTelemetry,\n listTelemetry,\n createAutomation,\n listAutomations,\n tickAutomations,\n enqueueCommand,\n listCommands,\n updateCommandStatus,\n registerSocialChannel,\n listSocialChannels\n} from \"./studioOpsStore.js\";\nimport { metrics as infraMetrics } from \"./infra/metrics.js\";\nimport { renderRhizohPrometheusMetrics, rhizohEnterpriseMetrics } from \"./infra/rhizohEnterpriseMetrics.js\";\nimport { scoreHealth } from \"./infra/healthScore.js\";\nimport { getTraceById, listTracesBySession } from \"./infra/traceRegistry.js\";\nimport { canonicalEpistemicSealString, hashAndSignEpistemicSeal, EPISTEMIC_SEAL_SCHEMA } from \"./epistemicSeal.js\";\nimport { persistEpistemicLedgerBatch } from \"./epistemicLedgerStore.js\";\nimport { persistEpistemicForecastBatch } from \"./epistemicForecastStore.js\";\nimport {\n persistAuthorityLedgerWitnessBatchV1,\n getAuthorityLedgerWitnessSnapshotV1,\n replayAuthorityLedgerWitnessChainV1\n} from \"./authorityLedgerWitnessStoreV1.js\";\nimport { assimilateAuthorityEpochMergeV1 } from \"./authorityEpochMergeStoreV1.js\";\nimport { buildRhizohExternalGroundTruthPayload } from \"./rhizohExternalGroundTruthGateway.js\";\nimport { ingestRhizohExternalLossBatchHttp } from \"./rhizohExternalLossIngestGateway.js\";\nimport {\n ingestRhizohProductOutcomeHttp,\n listRhizohProductOutcomeAggregates,\n verifyRhizohOutcomeHmac,\n verifyRhizohOutcomeSourceToken\n} from \"./rhizohProductOutcomeIngestGateway.js\";\nimport { initRhizoh } from \"./rhizohProductionBootstrap.js\";\nimport { handleAcademicObservatoryExportGetV0 } from \"./rhizoh/academicObservatoryHttpV0.js\";\nimport { handleCouncilAnomalyReasoningPostV0 } from \"./council/rhizohEpistemicCouncilHttpV0.js\";\nimport {\n handleLiveSportsBundleGetV0,\n handleLiveNewsHeadlinesGetV0,\n handleLiveWorldFeedGetV0\n} from \"./rhizoh/liveWorldFeedHttpV0.js\";\nimport {\n buildGenesisRuntimeSurfacePayload,\n recordGenesisEpistemicLedgerPersisted,\n recordGenesisEpistemicSealIssued,\n startGenesisCanonicalClock\n} from \"./genesisRuntimeSurfaceV0.js\";\nimport { registerGenesisSseClient, getGenesisContinuitySeq, setGenesisContinuityAfterPublishHook } from \"./genesisContinuityStreamHubV0.js\";\nimport { computeGenesisReplayFingerprintV0 } from \"./genesisReplayFingerprintV0.js\";\nimport { buildGenesisGatewayCapabilitiesV0 } from \"./genesisGatewayCapabilitiesV0.js\";\nimport {\n installGenesisCheckpointSurfaceGetter,\n noteGenesisCheckpointSeqCommitted,\n getLatestGenesisSignedCheckpoint\n} from \"./genesisContinuityCheckpointV0.js\";\nimport { hydrateGenesisContinuityPersistenceBootV0 } from \"./genesisContinuityHydrateBootV0.js\";\nimport { startGenesisContinuityInfraSampler } from \"./genesisContinuityInfraSamplerV0.js\";\nimport {\n genesisCheckpointQueryBySeqV0,\n genesisCheckpointQueryRangeV0,\n genesisCheckpointQueryLineageV0\n} from \"./genesisCheckpointQueryV0.js\";\nimport {\n genesisContinuityEventArchiveEnabled,\n queryGenesisContinuityEventArchiveV0,\n GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION\n} from \"./genesisContinuityEventArchiveV0.js\";\nimport { resolveGenesisReplayRouterV1, GENESIS_REPLAY_ROUTER_SCHEMA } from \"./genesisReplayRouterV1.js\";\nimport {\n computeGenesisReplayTemporalDiffV1,\n GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA\n} from \"./genesisReplayTemporalDiffV1.js\";\nimport { compareGenesisReplayEquivalenceV1 } from \"./genesisReplayEquivalenceV1.js\";\nimport { computeGenesisReplayAnalyticsV1 } from \"./genesisReplayAnalyticsV1.js\";\nimport { computeGenesisReplayEvolutionV1 } from \"./genesisReplayEvolutionV1.js\";\nimport { handleStripeCheckoutCreate, handleStripeMembershipWebhook } from \"./stripeWebhookMembership.js\";\nimport { buildRealityHealthPayload } from \"./edgeRealityConsistencyV1.js\";\nimport {\n assertWalPeerSocketAuthorityV0,\n buildGatewaySubstrateAuthoritySnapshotV0,\n logGatewaySubstrateAuthorityGuardsV0,\n validateWalPeerFeedSignatureV0\n} from \"./gatewaySubstrateAuthorityV0.js\";\nimport {\n buildSubstrateOperationalSnapshotV0,\n recordWalPeerRejectV0,\n renderSubstratePrometheusMetricsV0\n} from \"./infra/substrateOperationalMetrics.js\";\nimport { ingestClientSubstrateHealthV0 } from \"./rhizohSubstrateHealthIngestV0.js\";\nimport { recordSubstrateOtelEventV0 } from \"./infra/opentelemetrySubstrateV0.js\";\nimport {\n GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n getGenesisWorldObservationIngressStatusV0,\n ingestGenesisWorldObservationV0\n} from \"./genesisContinuityClientIngressV0.js\";\nimport {\n getGenesisContinuitySeqAuditV0,\n noteGenesisContinuitySeqAcceptedV0\n} from \"./genesisContinuitySeqGapV0.js\";\n\n/** Render / Fly / Railway: `PORT` — yoksa `CASTLE_GATEWAY_PORT` — yoksa 8090. */\nconst PORT =\n Number(process.env.PORT) ||\n Number(process.env.CASTLE_GATEWAY_PORT) ||\n 8090;\n/** Spiral / world WebSocket server — set after `wss` init; used by genesis runtime surface. */\nlet spiralWssForGenesis = null;\nconst MAX_MESSAGE_BYTES = Number(process.env.CASTLE_MAX_MESSAGE_BYTES || 32 * 1024);\nconst REQUIRED_GATEWAY_TOKEN = process.env.CASTLE_GATEWAY_TOKEN || \"\";\nconst ALLOWED_ORIGINS = parseAllowedOriginsFromEnv().fromList;\nconst httpCors = createHttpCorsPolicy();\n\n/** @returns {string|null} `Access-Control-Allow-Origin`; null = browser CORS fail. */\nfunction accessControlAllowOriginValue(req) {\n return httpCors.accessControlAllowOriginValue(req);\n}\n\nfunction applyHttpCorsHeaders(req, res) {\n return httpCors.applyHttpCorsHeaders(req, res);\n}\nconst REQUIRE_AUTH = process.env.CASTLE_REQUIRE_AUTH === \"true\";\nconst ALLOW_DEV_ANON = process.env.CASTLE_ALLOW_DEV_ANON !== \"false\";\nconst ALLOW_DEV_HTTP_UID = process.env.CASTLE_ALLOW_DEV_HTTP_UID !== \"false\";\n/** Presence mesh: allow `X-Castle-Guest-Id` when Firebase auth absent (default on). Set `false` to require auth only. */\nconst ALLOW_MESH_GUEST = process.env.CASTLE_ALLOW_MESH_GUEST !== \"false\";\nconst SOCIAL_RETRY_MAX = Math.max(0, Math.min(5, Number(process.env.CASTLE_SOCIAL_RETRY_MAX || 2)));\nconst SOCIAL_RETRY_BASE_MS = Math.max(100, Number(process.env.CASTLE_SOCIAL_RETRY_BASE_MS || 700));\nconst TELEGRAM_BOT_TOKEN = process.env.CASTLE_TELEGRAM_BOT_TOKEN || \"\";\nconst WHATSAPP_TOKEN = process.env.CASTLE_WHATSAPP_TOKEN || \"\";\nconst WHATSAPP_PHONE_NUMBER_ID = process.env.CASTLE_WHATSAPP_PHONE_NUMBER_ID || \"\";\nconst RL_RHIZOH_LLM_PER_MIN = Math.max(5, Number(process.env.CASTLE_RL_RHIZOH_LLM_PER_MIN || 40));\nconst RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN || 30)\n);\nconst RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN || 120));\nconst RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN = Math.max(\n 10,\n Number(process.env.CASTLE_RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN || 60)\n);\nconst RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN = Math.max(8, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN || 48));\nconst RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN = Math.max(4, Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN || 24));\nconst RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN || 18)\n);\nconst RL_EPISTEMIC_SEAL_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_EPISTEMIC_SEAL_PER_MIN || 120));\nconst RL_EPISTEMIC_LOG_PER_MIN = Math.max(20, Number(process.env.CASTLE_RL_EPISTEMIC_LOG_PER_MIN || 240));\nconst EPISTEMIC_SEAL_SECRET = String(\n process.env.CASTLE_EPISTEMIC_SEAL_SECRET || process.env.CASTLE_GATEWAY_TOKEN || \"\"\n).trim();\nconst RL_LLM_CONN_TEST_PER_MIN = Math.max(2, Number(process.env.CASTLE_RL_LLM_CONNECTION_TEST_PER_MIN || 8));\n\n/** One-shot production warnings: CORS surface, auth toggle, dev bypass flags (observatory deploy hygiene). */\nfunction logProductionObservatorySurfaceGuardsV0() {\n if (String(process.env.NODE_ENV || \"\").toLowerCase() !== \"production\") return;\n const tag = \"[GATEWAY_PROD_GUARD]\";\n const primaryCors = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n const corsListEmpty = ALLOWED_ORIGINS.length === 0;\n if (corsListEmpty && !primaryCors) {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty and CASTLE_HTTP_CORS_ORIGIN empty — production still merges RHIZOH_BROWSER_ORIGINS_V1 defaults; set env for preview/staging hosts`\n );\n } else if (corsListEmpty && primaryCors && primaryCors !== \"*\") {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty — only CASTLE_HTTP_CORS_ORIGIN set; add web.app + firebaseapp.com (and previews) to CASTLE_ALLOWED_ORIGINS if clients use alternate hostnames`\n );\n }\n if (primaryCors && /localhost|127\\.0\\.0\\.1/i.test(primaryCors)) {\n console.warn(`${tag} CORS: CASTLE_HTTP_CORS_ORIGIN is localhost while NODE_ENV=production`);\n }\n if (REQUIRE_AUTH) {\n console.warn(\n `${tag} AUTH: CASTLE_REQUIRE_AUTH=true — clients must send a valid gateway credential (otherwise silent 401 on replay/SSE); EventSource cannot set custom headers unless proxied`\n );\n if (REQUIRED_GATEWAY_TOKEN.length < 16) {\n console.warn(`${tag} AUTH: CASTLE_GATEWAY_TOKEN missing or short while require-auth enabled`);\n }\n }\n if (ALLOW_DEV_ANON) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_ANON is not \"false\" — dev anonymous path may be live in production (set CASTLE_ALLOW_DEV_ANON=false)`\n );\n }\n if (ALLOW_DEV_HTTP_UID) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_HTTP_UID is not \"false\" — X-Castle-Dev-Uid bypass risk (set CASTLE_ALLOW_DEV_HTTP_UID=false)`\n );\n }\n}\n\nconst rhizohRuntime = initRhizoh();\n\n/** İstemci: llmKeySource veya keySource — env | user_connection | auto */\nfunction normalizeClientLlmKeySource(raw) {\n const s = String(raw ?? \"\").trim().toLowerCase();\n if (s === \"env\" || s === \"server\") return \"env\";\n if (s === \"user_connection\" || s === \"connection\" || s === \"user\") return \"user_connection\";\n if (s === \"auto\" || s === \"\") return \"auto\";\n return null;\n}\n\n/**\n * Server-side provider wins for env/auto unless user_connection — client bundle may still send provider=openai.\n * @param {\"env\"|\"user_connection\"|\"auto\"} keyMode\n * @param {{ provider?: string, model?: string, apiKey?: string } | null} conn\n * @param {{ provider?: string, model?: string }} payload\n */\nfunction resolveServerLlmProviderV0(keyMode, conn, payload) {\n const envProvider = String(process.env.CASTLE_LLM_PROVIDER || \"\").trim() || undefined;\n const envModel = String(process.env.CASTLE_LLM_MODEL || \"\").trim() || undefined;\n\n if (keyMode === \"user_connection\") {\n return {\n resolvedProvider: conn?.provider || payload?.provider,\n resolvedModel: conn?.model || payload?.model,\n connApiKey: conn?.apiKey || \"\"\n };\n }\n\n if (keyMode === \"env\") {\n return {\n resolvedProvider: envProvider ?? payload?.provider,\n resolvedModel: envModel ?? payload?.model,\n connApiKey: \"\"\n };\n }\n\n if (conn?.provider && conn?.apiKey) {\n return {\n resolvedProvider: conn.provider,\n resolvedModel: conn.model,\n connApiKey: conn.apiKey\n };\n }\n\n return {\n resolvedProvider: envProvider ?? payload?.provider,\n resolvedModel: envModel ?? payload?.model,\n connApiKey: \"\"\n };\n}\n\nfunction jsonReplacerSafeV0(_key, value) {\n if (typeof value === \"bigint\") return Number(value);\n return value;\n}\n\nfunction sendJson(res, statusCode, payload) {\n res.writeHead(statusCode, { \"Content-Type\": \"application/json; charset=utf-8\" });\n try {\n res.end(JSON.stringify(payload, jsonReplacerSafeV0));\n } catch (error) {\n res.end(\n JSON.stringify({\n ok: false,\n error: \"response_serialize_failed\",\n detail: String(error?.message || error)\n })\n );\n }\n}\n\nfunction logRhizohHealth(stage, detail = {}) {\n try {\n console.info(`[CASTLE_${String(stage || \"unknown\")}]`, detail && typeof detail === \"object\" ? detail : {});\n } catch {\n /* noop */\n }\n}\n\nfunction renderInfraMetricsProm() {\n const lines = [\n `castle_gateway_events_processed_total ${infraMetrics.eventsProcessed}`,\n `castle_gateway_duplicate_reject_total ${infraMetrics.duplicateRejects}`,\n `castle_gateway_enqueue_latency_ms ${infraMetrics.enqueueLatencyMs}`,\n `castle_gateway_queue_depth ${infraMetrics.queueDepth}`,\n `castle_gateway_queue_lag_seconds ${(Number(infraMetrics.queueLag || 0) / 1000).toFixed(3)}`,\n `castle_gateway_errors_total ${infraMetrics.errors}`\n ];\n const buckets = infraMetrics.enqueueLatencyBuckets || {};\n for (const k of Object.keys(buckets)) {\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"${k}\"} ${buckets[k]}`);\n }\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"+Inf\"} ${infraMetrics.eventsProcessed}`);\n if (process.env.CASTLE_SUBSTRATE_METRICS !== \"0\") {\n lines.push(renderSubstratePrometheusMetricsV0().trimEnd());\n }\n return lines.join(\"\\n\");\n}\n\nasync function fetchJsonWithTimeout(url, timeoutMs = 1200) {\n const ctl = new AbortController();\n const tid = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const res = await fetch(url, { signal: ctl.signal });\n if (!res.ok) return null;\n return await res.json();\n } catch {\n return null;\n } finally {\n clearTimeout(tid);\n }\n}\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction getHttpPathname(req) {\n let p = String(req.url || \"\").split(\"?\")[0];\n /* Reverse proxies / clients sometimes append `/` — avoid silent 404 on e.g. `/rhizoh/genesis/stream/`. */\n while (p.length > 1 && p.endsWith(\"/\")) {\n p = p.slice(0, -1);\n }\n return p;\n}\n\nfunction rhizohLlmEnvConfigured() {\n const p = String(process.env.CASTLE_LLM_PROVIDER || \"openai\").toLowerCase();\n const key =\n p === \"anthropic\"\n ? process.env.ANTHROPIC_API_KEY\n : p === \"gemini\"\n ? process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY\n : p === \"xai\"\n ? process.env.XAI_API_KEY\n : p === \"deepseek\"\n ? process.env.DEEPSEEK_API_KEY\n : p === \"mistral\"\n ? process.env.MISTRAL_API_KEY\n : p === \"openrouter\"\n ? process.env.OPENROUTER_API_KEY\n : process.env.OPENAI_API_KEY;\n return !!String(key || \"\").trim();\n}\n\n/**\n * @param {import(\"firebase-admin/firestore\").Firestore | null} db\n * @param {number} timeoutMs\n */\nasync function probeFirestoreReachable(db, timeoutMs = 2000) {\n if (!db) return { ok: false, reason: \"no_db\" };\n const timeout = sleep(timeoutMs).then(() => ({ ok: false, reason: \"timeout\" }));\n const work = db\n .collection(\"_castle_gateway_health\")\n .doc(\"ping\")\n .get()\n .then(() => ({ ok: true, reason: \"reachable\" }))\n .catch((e) => ({ ok: false, reason: String(e?.message || e || \"firestore_error\") }));\n return Promise.race([work, timeout]);\n}\n\nfunction stripHtml(text = \"\") {\n return String(text)\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \" \")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \" \")\n .replace(/<[^>]+>/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction haversineMeters(aLat, aLon, bLat, bLon) {\n const R = 6371000;\n const dLat = ((bLat - aLat) * Math.PI) / 180;\n const dLon = ((bLon - aLon) * Math.PI) / 180;\n const sa = Math.sin(dLat / 2);\n const sb = Math.sin(dLon / 2);\n const x = sa * sa + Math.cos((aLat * Math.PI) / 180) * Math.cos((bLat * Math.PI) / 180) * sb * sb;\n return 2 * R * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));\n}\n\nasync function checkPublishTargetHealth(protocol, target) {\n const p = String(protocol || \"\").toUpperCase();\n const t = String(target || \"\").trim();\n if (!t) return { ok: false, reason: \"target_missing\", shouldReconnect: false };\n if (p === \"RTMP\") {\n const ok = t.startsWith(\"rtmp://\") || t.startsWith(\"rtmps://\");\n return { ok, reason: ok ? \"rtmp_target_format_ok\" : \"invalid_rtmp_target\", shouldReconnect: !ok, reconnectInSec: 15 };\n }\n if (p === \"WHIP\" || p === \"SFU\") {\n try {\n const res = await fetch(t, { method: \"OPTIONS\" });\n const ok = res.status < 500;\n return { ok, reason: `http_${res.status}`, shouldReconnect: !ok, reconnectInSec: 8 };\n } catch {\n return { ok: false, reason: \"network_unreachable\", shouldReconnect: true, reconnectInSec: 8 };\n }\n }\n return { ok: true, reason: \"unchecked_protocol\", shouldReconnect: false };\n}\n\nasync function fetchTextFromUrl(url) {\n const res = await fetch(url, { method: \"GET\" });\n if (!res.ok) throw new Error(`fetch_${res.status}`);\n const ct = String(res.headers.get(\"content-type\") || \"\");\n if (ct.includes(\"application/pdf\")) throw new Error(\"pdf_binary_not_supported_directly\");\n const body = await res.text();\n return stripHtml(body).slice(0, 14000);\n}\n\nfunction buildTaskActionGraph(task = \"\", context = {}) {\n const t = String(task || \"\").toLowerCase();\n if (t.includes(\"dinner\") || t.includes(\"akşam yemeği\") || t.includes(\"aksam yemegi\")) {\n return {\n taskTitle: \"Dinner Prep Planner\",\n nodes: [\n { id: \"n1\", title: \"Kitchen preheat\", action: \"smart-oven.preheat\", params: { temperatureC: 190, mode: \"fan\" } },\n { id: \"n2\", title: \"Lighting comfort\", action: \"smart-light.set_scene\", params: { scene: \"warm_dinner\" } },\n { id: \"n3\", title: \"Fridge inventory check\", action: \"smart-fridge.inventory_scan\", params: {} },\n { id: \"n4\", title: \"Reminder notification\", action: \"smart-watch.notify\", params: { text: \"Dinner prep started.\" } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" },\n { from: \"n3\", to: \"n4\" }\n ]\n };\n }\n return {\n taskTitle: task || \"General Planner\",\n nodes: [\n { id: \"n1\", title: \"Safety check\", action: \"device.safety_check\", params: { context } },\n { id: \"n2\", title: \"Primary action\", action: \"device.execute_task\", params: { task } },\n { id: \"n3\", title: \"Report\", action: \"studio.timeline_note\", params: { task } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" }\n ]\n };\n}\n\nfunction buildEthicsPrograms() {\n return {\n policyVersion: \"v1\",\n tracks: [\n {\n id: \"elderly-care-assist\",\n audience: \"elderly\",\n title: \"Elderly Assistance Track\",\n principles: [\"human-in-the-loop\", \"consent-first\", \"low-friction alerts\"],\n modules: [\"medication-reminder\", \"fall-risk-check\", \"family-notify\", \"daily-companion-brief\"]\n },\n {\n id: \"kids-safe-learning\",\n audience: \"children\",\n title: \"Kids Safe Learning Track\",\n principles: [\"guardian-consent\", \"age-appropriate-language\", \"content-filtering\"],\n modules: [\"study-coach\", \"screen-time-balance\", \"safe-social-share\", \"emergency-contact\"]\n }\n ]\n };\n}\n\nasync function deliverViaTelegram(channel, text) {\n const chatId = String(channel.endpoint || \"\").trim();\n if (!TELEGRAM_BOT_TOKEN) throw new Error(\"telegram_token_missing\");\n if (!chatId) throw new Error(\"telegram_chat_id_missing\");\n const url = `https://api.telegram.org/bot${encodeURIComponent(TELEGRAM_BOT_TOKEN)}/sendMessage`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ chat_id: chatId, text })\n });\n if (!res.ok) throw new Error(`telegram_http_${res.status}`);\n return { provider: \"telegram\", status: res.status };\n}\n\nasync function deliverViaWhatsapp(channel, text) {\n const to = String(channel.endpoint || \"\").trim();\n if (!WHATSAPP_TOKEN) throw new Error(\"whatsapp_token_missing\");\n if (!WHATSAPP_PHONE_NUMBER_ID) throw new Error(\"whatsapp_phone_number_id_missing\");\n if (!to) throw new Error(\"whatsapp_to_missing\");\n const url = `https://graph.facebook.com/v22.0/${encodeURIComponent(WHATSAPP_PHONE_NUMBER_ID)}/messages`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${WHATSAPP_TOKEN}` },\n body: JSON.stringify({ messaging_product: \"whatsapp\", to, type: \"text\", text: { body: text } })\n });\n if (!res.ok) throw new Error(`whatsapp_http_${res.status}`);\n return { provider: \"whatsapp\", status: res.status };\n}\n\nasync function deliverViaWebhook(channel, text) {\n const endpoint = String(channel.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"webhook_endpoint_missing\");\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text, platform: channel.platform || \"webhook\", ts: Date.now() })\n });\n if (!res.ok) throw new Error(`webhook_http_${res.status}`);\n return { provider: \"webhook\", status: res.status };\n}\n\nasync function deliverSocialMessage(channel, text) {\n const platform = String(channel.platform || \"webhook\").toLowerCase();\n if (platform === \"telegram\") return deliverViaTelegram(channel, text);\n if (platform === \"whatsapp\") return deliverViaWhatsapp(channel, text);\n return deliverViaWebhook(channel, text);\n}\n\nasync function deliverSocialWithRetry(channel, text) {\n let attempt = 0;\n let lastError = null;\n while (attempt <= SOCIAL_RETRY_MAX) {\n try {\n const result = await deliverSocialMessage(channel, text);\n return { ok: true, attempt: attempt + 1, ...result };\n } catch (error) {\n lastError = error;\n if (attempt >= SOCIAL_RETRY_MAX) break;\n const jitter = Math.floor(Math.random() * 90);\n await sleep(SOCIAL_RETRY_BASE_MS * Math.pow(2, attempt) + jitter);\n }\n attempt += 1;\n }\n return { ok: false, attempt: attempt + 1, error: String(lastError?.message || \"delivery_failed\") };\n}\n\nasync function dispatchCommandToAdapter(device, command) {\n const adapter = String(command?.adapter || device?.adapter || \"websocket\").toLowerCase();\n const endpoint = String(device?.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"device_endpoint_missing\");\n if (adapter === \"mqtt\") {\n // MQTT delivery is proxied via a bridge endpoint (HTTP webhook style).\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n type: \"mqtt-command\",\n topic: `castle/devices/${device.id}/commands`,\n action: command.action,\n params: command.params || {}\n })\n });\n if (!res.ok) throw new Error(`mqtt_bridge_http_${res.status}`);\n return { ok: true, adapter, via: \"http-bridge\", status: res.status };\n }\n if (!endpoint.startsWith(\"ws://\") && !endpoint.startsWith(\"wss://\")) throw new Error(\"invalid_websocket_endpoint\");\n const sendResult = await new Promise((resolve, reject) => {\n const ws = new WebSocket(endpoint);\n const timer = setTimeout(() => {\n try {\n ws.close();\n } catch {\n /* noop */\n }\n reject(new Error(\"websocket_timeout\"));\n }, 3000);\n ws.on(\"open\", () => {\n ws.send(\n JSON.stringify({\n type: \"castle-command\",\n commandId: command.id,\n deviceId: command.deviceId,\n action: command.action,\n params: command.params || {}\n }),\n (err) => {\n clearTimeout(timer);\n ws.close();\n if (err) reject(new Error(\"websocket_send_failed\"));\n else resolve({ ok: true, adapter: \"websocket\", via: \"ws\" });\n }\n );\n });\n ws.on(\"error\", () => {\n clearTimeout(timer);\n reject(new Error(\"websocket_connect_failed\"));\n });\n });\n return sendResult;\n}\n\nfunction readHttpJson(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => {\n if (!buf) return resolve({});\n const payload = safeJsonParse(buf);\n if (!payload || typeof payload !== \"object\") return reject(new Error(\"invalid_json_payload\"));\n resolve(payload);\n });\n req.on(\"error\", reject);\n });\n}\n\n/** Ham gövde (HMAC doğrulaması için). */\nfunction readHttpBodyText(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => resolve(buf));\n req.on(\"error\", reject);\n });\n}\n\nfunction maskOpaqueId(value) {\n const t = String(value || \"\");\n if (!t) return \"(empty)\";\n if (t.length <= 8) return \"****\";\n return `${t.slice(0, 4)}…${t.slice(-2)}`;\n}\n\nfunction readGatewayHttpToken(req) {\n const x = String(req.headers?.[\"x-castle-gateway-token\"] || \"\").trim();\n if (x) return x;\n const auth = String(req.headers?.authorization || \"\");\n const m = /^Bearer\\s+(.+)$/i.exec(auth);\n return m ? String(m[1] || \"\").trim() : \"\";\n}\n\nasync function resolveHttpUser(req) {\n const authResult = await verifyClientToken(req);\n if (authResult?.ok && authResult.user?.uid) {\n return { ok: true, uid: String(authResult.user.uid), mode: authResult.kind || \"auth\" };\n }\n if (ALLOW_DEV_ANON && ALLOW_DEV_HTTP_UID) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) return { ok: true, uid: `dev-${devUid.slice(0, 64)}`, mode: \"dev-header\" };\n }\n return { ok: false, reason: authResult?.reason || \"auth_required\" };\n}\n\n/**\n * Epistemic ledger ingest — Firebase uid OR valid transport token + X-Castle-Dev-Uid.\n * Prod disables CASTLE_ALLOW_DEV_HTTP_UID; LLM may still work while telemetry 401'd without this.\n */\nasync function resolveEpistemicIngestActor(req) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok && tok === REQUIRED_GATEWAY_TOKEN) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) {\n return { ok: true, uid: `gw-telemetry-${devUid.slice(0, 64)}`, mode: \"gateway-token-dev-uid\" };\n }\n return { ok: false, reason: \"dev_uid_required\" };\n }\n if (tok) {\n return { ok: false, reason: \"gateway_token_mismatch\" };\n }\n }\n const auth = await resolveHttpUser(req);\n if (auth.ok) return auth;\n return { ok: false, reason: auth.reason || \"auth_required\" };\n}\n\n/** Mesh actor: Firebase uid, dev header, or `guest:<uuid>` via header / `guestId` query (SSE cannot set headers). */\nasync function resolveMeshActor(req) {\n const auth = await resolveHttpUser(req);\n if (auth.ok) return { ok: true, clientUid: auth.uid, mode: auth.mode };\n if (ALLOW_MESH_GUEST) {\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const gid = String(req.headers?.[\"x-castle-guest-id\"] || q.searchParams.get(\"guestId\") || \"\").trim();\n if (gid) {\n const normalized = gid.startsWith(\"guest:\") ? gid.slice(0, 180) : `guest:${gid.slice(0, 160)}`;\n return { ok: true, clientUid: normalized, mode: \"guest-header\" };\n }\n }\n return { ok: false, reason: \"mesh_auth_required\" };\n}\n\n/** Live genesis runtime JSON (GET /rhizoh/genesis/runtime + SSE infra sampler). */\nasync function buildGenesisRuntimeSurfacePayloadLive() {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n let spiralWsActive = 0;\n if (spiralWssForGenesis) {\n for (const c of spiralWssForGenesis.clients) {\n if (c.readyState === 1) spiralWsActive += 1;\n }\n }\n const base = buildGenesisRuntimeSurfacePayload({\n infraMetrics,\n scoredHealth: scored,\n rhizohEnterpriseMetrics,\n mesh: meshContinuityAggregate(),\n workerHealth: worker,\n port: PORT,\n spiralWebSocketClientsActive: spiralWsActive\n });\n return {\n ...base,\n replayFingerprint: computeGenesisReplayFingerprintV0(base),\n gatewayCapabilities: buildGenesisGatewayCapabilitiesV0(),\n genesisStream: {\n schema: \"castle.genesis.stream_cursor.v0\",\n lastAcceptedSeq: getGenesisContinuitySeq(),\n clientIngress: {\n schema: GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n contract: \"castle.world_observation.ingress_envelope.v1\",\n postPath: rhizohRuntime.routes.genesisIngress,\n closure: getGenesisWorldObservationIngressStatusV0()\n },\n seqContinuity: getGenesisContinuitySeqAuditV0(),\n eventArchive: genesisContinuityEventArchiveEnabled()\n ? {\n enabled: true,\n projection: GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION,\n queryPath: rhizohRuntime.routes.genesisContinuityEvents\n }\n : { enabled: false },\n replayRouter: {\n schema: GENESIS_REPLAY_ROUTER_SCHEMA,\n queryPath: rhizohRuntime.routes.genesisReplay,\n queryHint: \"from=&to= or range=from-to; optional type=&checkpoints=0\",\n temporalDiffPath: rhizohRuntime.routes.genesisReplayDiff,\n temporalDiffSchema: GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA,\n equivalencePath: rhizohRuntime.routes.genesisReplayEquivalence,\n analyticsPath: rhizohRuntime.routes.genesisReplayAnalytics,\n evolutionPath: rhizohRuntime.routes.genesisReplayEvolution\n }\n }\n };\n}\n\nconst httpServer = createServer(async (req, res) => {\n const corsAllow = applyHttpCorsHeaders(req, res);\n if (req.method === \"OPTIONS\") {\n if (!corsAllow && req.headers?.origin) {\n console.warn(\"[GATEWAY_CORS] preflight rejected for origin:\", req.headers.origin);\n }\n res.writeHead(corsAllow ? 204 : 403);\n res.end();\n return;\n }\n\n const pathname = getHttpPathname(req);\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/genesis/__ping\") {\n sendJson(res, 200, {\n ok: true,\n pid: process.pid,\n entry: __filename,\n time: Date.now(),\n genesis: true\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/epistemic/__probe\") {\n sendJson(res, 200, {\n ok: true,\n epistemicIngestActorV2: true,\n routes: {\n seal: rhizohRuntime.routes.epistemicSeal,\n logsBatch: rhizohRuntime.routes.epistemicLogsBatch,\n authorityLedgerBatch: rhizohRuntime.routes.authorityLedgerBatch,\n authorityLedgerReplay: rhizohRuntime.routes.authorityLedgerReplay,\n authorityEpochMerge: rhizohRuntime.routes.authorityEpochMerge\n },\n hasGatewayToken: Boolean(REQUIRED_GATEWAY_TOKEN),\n gatewayTokenLen: REQUIRED_GATEWAY_TOKEN ? REQUIRED_GATEWAY_TOKEN.length : 0\n });\n return;\n }\n\n if (\n req.method === \"GET\" &&\n (pathname === \"/rhizoh/live/world-feed\" ||\n pathname === \"/rhizoh/live/sports-bundle\" ||\n pathname === \"/rhizoh/live/news-headlines\")\n ) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_live_world_feed:${ip}`, RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const out =\n pathname === \"/rhizoh/live/sports-bundle\"\n ? await handleLiveSportsBundleGetV0()\n : pathname === \"/rhizoh/live/news-headlines\"\n ? await handleLiveNewsHeadlinesGetV0(req)\n : await handleLiveWorldFeedGetV0(req);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/webhooks/stripe\") {\n await handleStripeMembershipWebhook(req, res);\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/billing/stripe/checkout\") {\n await handleStripeCheckoutCreate(req, res, { readHttpJson, resolveHttpUser, sendJson });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/live\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, live: false, dns: true, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n sendJson(res, 200, { ok: true, live: true, dns: true, service: \"castle-gateway\", ts: Date.now() });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/cors\") {\n const reqOrigin = normalizeHttpOrigin(req.headers?.origin);\n const allowOrigin = accessControlAllowOriginValue(req);\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n requestOrigin: reqOrigin || null,\n allowed: Boolean(allowOrigin),\n allowOrigin: allowOrigin || null,\n allowSetSize: httpCors.allowSet.size\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/ready\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, ready: false, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n try {\n const persistence = getFirebasePersistence();\n const llmProbe = rhizohLlmEnvConfigured();\n sendJson(res, 200, {\n ok: true,\n ready: true,\n dns: true,\n persistence: persistence.mode,\n service: \"castle-gateway\",\n gateway: \"ok\",\n llm: llmProbe ? \"ok\" : \"degraded\",\n spine: llmProbe ? \"ok\" : \"partial\"\n });\n } catch (e) {\n sendJson(res, 503, { ok: false, ready: false, reason: String(e?.message || e), service: \"castle-gateway\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/reality\") {\n const payload = buildRealityHealthPayload();\n sendJson(res, payload.ok ? 200 : 503, payload);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/substrate\") {\n const authority = buildGatewaySubstrateAuthoritySnapshotV0();\n const operational = buildSubstrateOperationalSnapshotV0();\n const drift = buildRealityHealthPayload();\n const ok = Boolean(authority.productionAuthoritySatisfied) && drift.ok;\n sendJson(res, ok ? 200 : 503, {\n ok,\n service: \"castle-gateway\",\n ts: Date.now(),\n authority,\n operational,\n drift: { ok: drift.ok, errors: drift.drift?.errors, warnings: drift.drift?.warnings }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.councilAnomalyReasoning) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_council_anomaly:${ip}`, 30, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const body = await readHttpJson(req);\n const out = handleCouncilAnomalyReasoningPostV0(body);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/substrate/health\") {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const body = await readHttpJson(req);\n const uid = (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestClientSubstrateHealthV0(body, { uid, clientId: uid });\n if (!result.ok) {\n sendJson(res, 400, result);\n return;\n }\n recordSubstrateOtelEventV0(\"client_health_ingest\", { uid: String(uid).slice(0, 32) });\n sendJson(res, 200, result);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/deps\") {\n const t0 = Date.now();\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, {\n ok: false,\n dns: true,\n llm: false,\n firestore: false,\n phase: \"maintenance\",\n latencyMs: Date.now() - t0,\n service: \"castle-gateway\"\n });\n return;\n }\n const persistence = getFirebasePersistence();\n let firestoreOk = persistence.mode !== \"firebase\";\n let firestoreDetail = persistence.mode === \"file\" ? \"file_mode\" : \"not_applicable\";\n if (persistence.mode === \"firebase\" && persistence.db) {\n const pr = await probeFirestoreReachable(persistence.db, 2000);\n firestoreOk = pr.ok;\n firestoreDetail = pr.reason || (pr.ok ? \"reachable\" : \"fail\");\n } else if (persistence.mode === \"firebase\" && !persistence.db) {\n firestoreOk = false;\n firestoreDetail = \"firebase_not_initialized\";\n }\n const llmOk = rhizohLlmEnvConfigured();\n const overallOk = llmOk && firestoreOk;\n const latencyMs = Date.now() - t0;\n sendJson(res, 200, {\n ok: overallOk,\n dns: true,\n llm: llmOk,\n firestore: persistence.mode === \"firebase\" ? firestoreOk : false,\n persistence: persistence.mode,\n firestoreDetail,\n latencyMs,\n service: \"castle-gateway\",\n wsPort: PORT\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health\") {\n const persistence = getFirebasePersistence();\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n wsPort: PORT,\n persistence: persistence.mode,\n presenceMesh: \"sse\"\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/health\") {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n sendJson(res, 200, {\n ok: true,\n role: \"gateway\",\n queueLagMs: infraMetrics.queueLag,\n queueDepth: infraMetrics.queueDepth,\n errors: infraMetrics.errors,\n status: scored.status,\n score: scored.score,\n reasons: scored.reasons,\n worker: worker\n ? {\n status: worker.status,\n score: worker.score,\n reasons: worker.reasons\n }\n : null,\n timestamp: Date.now()\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisRuntime) {\n const payload = await buildGenesisRuntimeSurfacePayloadLive();\n sendJson(res, 200, payload);\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.genesisIngress) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const actor = await resolveMeshActor(req);\n const clientId = actor.ok ? actor.clientUid : (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestGenesisWorldObservationV0(body, { clientId });\n if (!result.ok) {\n const status =\n result.error === \"rate_limited\" || result.error === \"agent_spoke_throttled\"\n ? 429\n : result.error === \"payload_too_large\"\n ? 413\n : 400;\n sendJson(res, status, result);\n return;\n }\n sendJson(res, result.idempotent ? 200 : 202, result);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisStream) {\n const corsOrigin = accessControlAllowOriginValue(req);\n if (!corsOrigin && String(process.env.NODE_ENV || \"\").toLowerCase() === \"production\") {\n const og = String(req.headers?.origin || \"\").trim();\n if (og) {\n console.warn(\"[GATEWAY] genesis SSE: missing Access-Control-Allow-Origin for browser Origin (CORS whitelist mismatch):\", og);\n }\n }\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n res.write(\"retry: 3000\\n\\n\");\n res.write(\": genesis continuity stream\\n\\n\");\n res.write(\"event: genesis\\n\");\n res.write(`data: {\"booted\":true,\"pid\":${process.pid}}\\n\\n`);\n registerGenesisSseClient(res, req);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLatest) {\n const cp = getLatestGenesisSignedCheckpoint();\n if (!cp) {\n sendJson(res, 404, { ok: false, error: \"no_checkpoint_yet\", hint: \"wait_for_seq_interval_or_configure_signing_secret\" });\n return;\n }\n sendJson(res, 200, { ok: true, checkpoint: cp });\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(rhizohRuntime.routes.genesisCheckpointBySeqPrefix)) {\n const tail = pathname.slice(rhizohRuntime.routes.genesisCheckpointBySeqPrefix.length).split(\"/\")[0];\n const seq = parseInt(String(tail || \"\").trim(), 10);\n const out = await genesisCheckpointQueryBySeqV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"checkpoint_not_found\") {\n sendJson(res, 404, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointRange) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryRangeV0(from, to);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"range_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLineage) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const seq = parseInt(String(u.searchParams.get(\"seq\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryLineageV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"lineage_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisContinuityEvents) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const limit = parseInt(String(u.searchParams.get(\"limit\") || \"\").trim(), 10);\n const out = await queryGenesisContinuityEventArchiveV0(from, to, type, limit);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\" || out.error === \"event_archive_disabled\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"archive_file_too_large\") {\n sendJson(res, 413, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplay) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await resolveGenesisReplayRouterV1({\n from: fromSeq,\n to: toSeq,\n type,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayDiff) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const out = await computeGenesisReplayTemporalDiffV1({ from: fromSeq, to: toSeq, type });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEquivalence) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from1 = parseInt(String(u.searchParams.get(\"from1\") || \"\").trim(), 10);\n const to1 = parseInt(String(u.searchParams.get(\"to1\") || \"\").trim(), 10);\n const from2 = parseInt(String(u.searchParams.get(\"from2\") || \"\").trim(), 10);\n const to2 = parseInt(String(u.searchParams.get(\"to2\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await compareGenesisReplayEquivalenceV1({\n from1,\n to1,\n from2,\n to2,\n type,\n includeCheckpoints\n });\n if (\n !out.ok &&\n (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")\n ) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayAnalytics) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayAnalyticsV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEvolution) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const collapseWindows = parseInt(String(u.searchParams.get(\"collapseWindows\") || \"0\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayEvolutionV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n collapseWindows,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/metrics\") {\n res.writeHead(200, { \"Content-Type\": \"text/plain; version=0.0.4; charset=utf-8\" });\n res.end(renderInfraMetricsProm());\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.infraPrometheusMetrics) {\n if (process.env.CASTLE_PROMETHEUS_METRICS !== \"1\") {\n sendJson(res, 404, { ok: false, error: \"prometheus_metrics_disabled\" });\n return;\n }\n res.writeHead(200, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(renderRhizohPrometheusMetrics());\n return;\n }\n if (req.method === \"GET\" && pathname.startsWith(\"/infra/traces/\")) {\n const traceId = decodeURIComponent(pathname.slice(\"/infra/traces/\".length));\n const item = await getTraceById(traceId);\n if (!item) return sendJson(res, 404, { ok: false, error: \"trace_not_found\" });\n return sendJson(res, 200, { ok: true, trace: item });\n }\n if (req.method === \"GET\" && pathname === \"/infra/traces\") {\n const u = new URL(String(req.url || \"/infra/traces\"), `http://${req.headers.host || \"localhost\"}`);\n const sessionId = String(u.searchParams.get(\"sessionId\") || \"\").trim();\n const items = sessionId ? await listTracesBySession(sessionId) : [];\n return sendJson(res, 200, { ok: true, sessionId, traces: items });\n }\n\n /** Presence room mesh (C): JOIN / LEAVE / SNAPSHOT / DELTA / REPLAY (HTTP); SUBSCRIBE = SSE stream. */\n if (req.method === \"POST\" && pathname === \"/presence/mesh/join\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid, clientUid: actor.clientUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/leave\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshLeave(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/snapshot\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n sendJson(res, 200, meshSnapshot(roomUid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/delta\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, MAX_MESSAGE_BYTES);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const seq = meshAppendDelta(roomUid, actor.clientUid, {\n node: body?.node,\n projectionPatch: body?.projectionPatch,\n writerSubject: body?.writerSubject\n });\n sendJson(res, 200, { ok: true, roomUid, seq });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/replay\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const fromNodeId = body?.fromNodeId != null ? String(body.fromNodeId) : \"\";\n const fromSeq = body?.fromSeq;\n if (fromNodeId) {\n sendJson(res, 200, meshReplayFromNodeId(roomUid, fromNodeId));\n return;\n }\n sendJson(res, 200, meshReplayFromSeq(roomUid, fromSeq));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/presence/mesh/subscribe\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const roomUid = String(q.searchParams.get(\"roomUid\") || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n const write = (chunk) => {\n try {\n res.write(chunk);\n } catch {\n /* closed */\n }\n };\n const unsub = meshSubscribeSse(roomUid, write);\n const onClose = () => {\n unsub();\n meshLeave(roomUid, actor.clientUid);\n try {\n res.end();\n } catch {\n /* ignore */\n }\n };\n req.on(\"close\", onClose);\n req.on(\"aborted\", onClose);\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, uid: auth.uid, mode: auth.mode, items: await listConnections(auth.uid) });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const id = await createConnection(auth.uid, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"create_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n await updateConnection(auth.uid, id, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"update_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"DELETE\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n await deleteConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"delete_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url?.startsWith(\"/rhizoh/network/presence\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const u = new URL(req.url, \"http://localhost\");\n const roomKey = String(u.searchParams.get(\"room\") || \"world_space_c2c_v0\").slice(0, 64);\n sendJson(res, 200, {\n ok: true,\n roomKey,\n presence: listCastleNetworkPresenceV0(roomKey)\n });\n } catch (error) {\n sendJson(res, 500, { ok: false, error: error?.message || \"presence_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/rhizoh/sync/vault\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const snapshot = await getCloudSyncSnapshotV0(auth.uid);\n sendJson(res, 200, { ok: true, uid: auth.uid, snapshot });\n } catch (error) {\n sendJson(res, 500, { ok: false, error: error?.message || \"cloud_sync_pull_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/rhizoh/sync/vault\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const snapshot = await mergeCloudSyncSnapshotV0(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, uid: auth.uid, snapshot });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"cloud_sync_push_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections/test\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n if (!checkHttpRateLimit(`llm_conn_test:${auth.uid}`, RL_LLM_CONN_TEST_PER_MIN, 60_000)) {\n return sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n }\n const hasBodyKey = !!String(payload?.apiKey || \"\").trim();\n const result = await queryRhizohLlm(\n {\n message: String(payload?.message || \"Ping from Castle Gateway\"),\n context: payload?.context || { source: \"connection_test\" },\n provider: payload?.provider,\n model: payload?.model,\n apiKey: payload?.apiKey || \"\"\n },\n { allowExternalApiKey: true, llmKeySource: hasBodyKey ? \"auto\" : \"env\" }\n );\n logLlmAccess({\n route: \"/llm/connections/test\",\n uid: auth.uid,\n llmKeyBillingOwner: result.llmKeyBillingOwner,\n llmKeyOrigin: result.llmKeyOrigin,\n provider: result.provider,\n model: result.model,\n connectionId: null\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"connection_test_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAgentIdentities(auth.uid);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerAgentIdentity({\n ownerUid: auth.uid,\n agentId: payload?.agentId,\n role: payload?.role,\n personaSeed: payload?.personaSeed || {},\n capabilityLevel: payload?.capabilityLevel\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/agents/identities/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const owns = await getAgentIdentity(id);\n if (!owns || owns.ownerUid !== auth.uid) return sendJson(res, 403, { ok: false, error: \"forbidden\" });\n const payload = await readHttpJson(req);\n const row = await updateAgentIdentity(id, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/academy/events\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/academy/events\"}`);\n const status = String(url.searchParams.get(\"status\") || \"\");\n const items = await listAcademyEvents({ ownerUid: auth.uid, status, limit: 120 });\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/academy/events\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const ev = await queueAcademyEvent({\n type: payload?.type,\n roomId: payload?.roomId,\n ownerUid: auth.uid,\n participants: payload?.participants || [],\n topic: payload?.topic || \"academy\",\n payload: payload?.payload || {}\n });\n sendJson(res, 200, { ok: true, event: ev });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && String(req.url || \"\").startsWith(\"/academy/events/\") && String(req.url || \"\").endsWith(\"/resolve\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const parts = String(req.url || \"\").split(\"/\").filter(Boolean);\n const eventId = parts[2] || \"\";\n try {\n const payload = await readHttpJson(req);\n const out = await resolveAcademyEvent(eventId, payload || {});\n sendJson(res, 200, { ok: true, event: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_resolve_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/memory\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/memory\"}`);\n const scope = url.searchParams.get(\"scope\") === \"agents\" ? \"agents\" : \"users\";\n const id = String(url.searchParams.get(\"id\") || (scope === \"agents\" ? \"\" : auth.uid));\n const kind = String(url.searchParams.get(\"kind\") || \"\");\n const limit = Number(url.searchParams.get(\"limit\") || 40);\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n sendJson(res, 200, { ok: true, scope, id, kind, items: await listMemories({ scope, id, kind, limit }) });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const profile = await getPersonaGoalMemory(auth.uid);\n sendJson(res, 200, { ok: true, profile });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const profile = await setPersonaGoalMemory({\n uid: auth.uid,\n personaPatch: payload?.personaPatch || {},\n goals: Array.isArray(payload?.goals) ? payload.goals : null,\n preferencesPatch: payload?.preferencesPatch || {}\n });\n sendJson(res, 200, { ok: true, profile });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"profile_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/compact\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const out = await autoCompactMemories({ scope, id });\n sendJson(res, 200, { ok: true, scope, id, ...out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_compact_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/context\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const context = await getMemoryContext({\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n query: String(payload?.query || \"\"),\n limit: Number(payload?.limit || 80)\n });\n sendJson(res, 200, { ok: true, context });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_context_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/place-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const placeName = String(payload?.placeName || \"\").trim();\n if (!placeName) return sendJson(res, 400, { ok: false, error: \"placeName_required\" });\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const persona = String(payload?.waypointPersona || payload?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(placeName)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Place: ${placeName}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Give a concise conversational briefing about this place for a walking-route companion.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-place-brief\", placeName, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `PLACE_BRIEF:${placeName} => ${String(summary.reply || \"\").slice(0, 900)}`,\n tags: [\"place\", \"brief\", \"event-layer\"],\n importance: 0.62,\n meta: { placeName }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"place-brief\",\n text: `${placeName} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: {\n placeName,\n directive: summary.directive,\n lat: hit?.lat ? Number(hit.lat) : null,\n lon: hit?.lon ? Number(hit.lon) : null\n }\n });\n sendJson(res, 200, { ok: true, place: hit || null, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"place_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/route-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const waypoints = Array.isArray(payload?.waypoints) ? payload.waypoints.slice(0, 24) : [];\n const out = [];\n for (const wp of waypoints) {\n const name = String(wp?.name || \"\").trim();\n if (!name) continue;\n const persona = String(wp?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(name)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Waypoint: ${name}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Generate a concise spoken briefing for route waypoint.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-route-brief\", waypoint: name, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n out.push({\n name,\n persona,\n place: hit || null,\n reply: summary.reply,\n directive: summary.directive\n });\n }\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `ROUTE_BRIEF: waypoints=${out.length} ${out.map((x) => `${x.name}:${x.persona}`).join(\" | \")}`.slice(0, 1300),\n tags: [\"route\", \"waypoint\", \"brief\", \"event-layer\"],\n importance: 0.74,\n meta: { count: out.length }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"route-brief\",\n text: out.map((x) => `${x.name}:${String(x.reply || \"\").slice(0, 140)}`).join(\" | \").slice(0, 1400),\n roomId: \"studio-main\",\n meta: {\n waypointCount: out.length,\n waypoints: out.map((x) => ({\n name: x.name,\n persona: x.persona,\n lat: x.place?.lat ? Number(x.place.lat) : null,\n lon: x.place?.lon ? Number(x.place.lon) : null\n }))\n }\n });\n sendJson(res, 200, { ok: true, items: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"route_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/pdf-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const title = String(payload?.title || \"document\");\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const providedText = String(payload?.text || \"\");\n const url = String(payload?.url || \"\");\n let content = providedText;\n if (!content && url) content = await fetchTextFromUrl(url);\n if (!content) return sendJson(res, 400, { ok: false, error: \"text_or_supported_url_required\" });\n const summary = await queryRhizohLlm(\n {\n message: `Summarize this document and provide key points + practical actions.\\nTitle:${title}\\nContent:\\n${content.slice(0, 12000)}`,\n context: { source: \"event-layer-pdf-brief\", title },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `DOC_BRIEF:${title} => ${String(summary.reply || \"\").slice(0, 1000)}`,\n tags: [\"document\", \"brief\", \"library\", \"event-layer\"],\n importance: 0.7,\n meta: { title, url: url || null }\n });\n await addTranscript(auth.uid, {\n source: \"library-brief\",\n eventType: \"pdf-brief\",\n text: `${title} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { title }\n });\n sendJson(res, 200, { ok: true, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/pdf/upload\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req, 8 * 1024 * 1024);\n const job = await enqueuePdfJob({\n uid: auth.uid,\n fileName: payload?.fileName || \"document.pdf\",\n bytesBase64: payload?.contentBase64\n });\n sendJson(res, 200, { ok: true, job });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_upload_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/session\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createPublishSession(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session\",\n text: `Publish session armed: ${row.protocol} ${row.target}`.slice(0, 500),\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: row.id, bridge: row.bridge }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/studio/publish/sessions\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listPublishSessions(auth.uid, 80);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/health-check\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const protocol = String(payload?.protocol || \"WHIP\");\n const target = String(payload?.target || \"\");\n const health = await checkPublishTargetHealth(protocol, target);\n if (payload?.sessionId) {\n await updatePublishSession(auth.uid, String(payload.sessionId), {\n status: health.ok ? \"healthy\" : \"degraded\",\n reconnectPolicy: {\n enabled: true,\n backoffSec: [2, 5, 10, 20],\n nextRetryInSec: health.reconnectInSec || 0,\n maxRetries: 8,\n reason: health.reason\n }\n });\n }\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-health\",\n text: `${protocol} ${target} => ${health.ok ? \"healthy\" : \"degraded\"} (${health.reason})`,\n roomId: \"studio-main\",\n meta: { protocol, target, reconnect: !!health.shouldReconnect }\n });\n sendJson(res, 200, { ok: true, health });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_health_check_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/studio/publish/sessions/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updatePublishSession(auth.uid, id, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session-update\",\n text: `Session ${id} -> ${row.status || \"updated\"}`,\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: id, status: row.status, ingestUrl: row.ingestUrl || \"\" }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/transcripts\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/transcripts\"}`);\n const limit = Math.max(1, Math.min(200, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await listTranscripts(auth.uid, limit);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/transcripts\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await addTranscript(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"transcript_write_failed\" });\n }\n return;\n }\n\n /** E1: broadcast intent → GreenRoom capability → transcript + memory artifact */\n if (req.method === \"POST\" && req.url === \"/studio/capabilities/greenroom\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const traceId = String(payload?.traceId || `TRC-${Date.now().toString(36).toUpperCase()}`);\n const title = String(payload?.title || \"Castle Live Broadcast\").slice(0, 200);\n const intentRaw = String(payload?.intentRaw || \"\").slice(0, 800);\n const audienceEstimate = Math.max(0, Math.min(50_000_000, Number(payload?.audienceEstimate) || 1200));\n const roomId = String(payload?.roomId || \"greenroom-main\");\n const transcriptRow = await addTranscript(auth.uid, {\n source: \"rhizoh\",\n eventType: \"BROADCAST_ROUTED\",\n text: `GreenRoom · ${title}${intentRaw ? `\\nIntent: ${intentRaw}` : \"\"}`,\n roomId,\n meta: {\n ack: \"BROADCAST_ROUTED\",\n capability: \"greenroom\",\n intentType: \"broadcast\",\n traceId,\n audienceEstimate,\n artifact: {\n kind: \"broadcast_card\",\n title,\n traceId,\n roomId,\n createdAt: Date.now()\n }\n }\n });\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n text: `GREENROOM_BROADCAST:${title} trace=${traceId}`,\n tags: [\"broadcast\", \"greenroom\", \"artifact\", \"castle-library\"],\n importance: 0.72,\n kind: \"episodic\",\n meta: { traceId, capability: \"greenroom\", transcriptId: transcriptRow.id }\n });\n sendJson(res, 200, {\n ok: true,\n ack: \"BROADCAST_ROUTED\",\n traceId,\n transcript: transcriptRow,\n replayPath: `/replay/${encodeURIComponent(traceId)}`,\n sharePath: `/replay/${encodeURIComponent(traceId)}`\n });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"greenroom_capability_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/metadata/search\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/metadata/search\"}`);\n const q = String(url.searchParams.get(\"q\") || \"\");\n const limit = Math.max(1, Math.min(300, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await searchMetadata(auth.uid, q, limit);\n sendJson(res, 200, { ok: true, q, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerDevice(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, device: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"device_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const lat = Number(payload?.lat);\n const lon = Number(payload?.lon);\n const speed = Math.max(0, Number(payload?.speed || 0));\n const cfgCenterLat = Number(process.env.CASTLE_GEOFENCE_CENTER_LAT || 41.0082);\n const cfgCenterLon = Number(process.env.CASTLE_GEOFENCE_CENTER_LON || 28.9784);\n const cfgRadiusM = Math.max(100, Number(process.env.CASTLE_GEOFENCE_RADIUS_M || 55000));\n const cfgSpeedCap = Math.max(1, Number(process.env.CASTLE_SPEED_CAP_MPS || 35));\n const dist = Number.isFinite(lat) && Number.isFinite(lon) ? haversineMeters(cfgCenterLat, cfgCenterLon, lat, lon) : 0;\n const geofenceOk = !Number.isFinite(dist) ? true : dist <= cfgRadiusM;\n const speedOk = speed <= cfgSpeedCap;\n const failsafe = !(geofenceOk && speedOk);\n const safety = {\n geofenceOk,\n speedOk,\n failsafe,\n distanceFromCenterM: Number.isFinite(dist) ? dist : null,\n speedCapMps: cfgSpeedCap\n };\n const row = await appendTelemetry(auth.uid, deviceId, {\n lat,\n lon,\n speed,\n battery: payload?.battery,\n mode: payload?.mode || (failsafe ? \"FAILSAFE_HOLD\" : \"ACTIVE\"),\n raw: payload?.raw || {}\n });\n await updateDevice(auth.uid, deviceId, {\n status: failsafe ? \"failsafe\" : \"active\",\n lastTelemetryAt: row.ts,\n safetyState: safety\n });\n if (failsafe) {\n await addTranscript(auth.uid, {\n source: \"robotics-guardrails\",\n eventType: \"failsafe\",\n text: `Device ${deviceId} failsafe triggered geofenceOk=${geofenceOk} speedOk=${speedOk}`,\n roomId: \"studio-main\",\n meta: { deviceId, safety }\n });\n }\n sendJson(res, 200, { ok: true, telemetry: row, safety, command: failsafe ? \"FAILSAFE_HOLD\" : \"CONTINUE\" });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"telemetry_ingest_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listTelemetry(auth.uid, 150);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createAutomation(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"automation-scheduled\",\n text: `Automation scheduled: ${row.title} at ${new Date(row.scheduleAt).toLocaleString()}`,\n roomId: \"studio-main\",\n meta: { automationId: row.id, action: row.action, deviceId: row.deviceId }\n });\n sendJson(res, 200, { ok: true, automation: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"automation_create_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAutomations(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listDevices(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const devs = await listDevices(auth.uid, 500);\n const d = devs.find((x) => x.id === deviceId);\n if (!d) return sendJson(res, 404, { ok: false, error: \"device_not_found\" });\n const action = String(payload?.action || \"\");\n const caps = Array.isArray(d.capabilityProfile) ? d.capabilityProfile : [];\n if (action && caps.length && !caps.includes(action) && !caps.includes(\"*\")) {\n return sendJson(res, 400, { ok: false, error: \"unsupported_action_for_device\", capabilityProfile: caps });\n }\n const cmd = await enqueueCommand(auth.uid, {\n deviceId,\n action: action || \"NOOP\",\n params: payload?.params || {},\n adapter: payload?.adapter || d.adapter || \"websocket\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-command-queue\",\n eventType: \"command-queued\",\n text: `${cmd.deviceId} -> ${cmd.action}`,\n roomId: \"studio-main\",\n meta: { commandId: cmd.id, adapter: cmd.adapter }\n });\n sendJson(res, 200, { ok: true, command: cmd });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listCommands(auth.uid, 200);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/robotics/commands/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updateCommandStatus(auth.uid, id, payload?.status || \"sent\");\n sendJson(res, 200, { ok: true, command: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/rhizoh/bridge\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const result = await runRhizohBrainV2({\n message: String(payload?.message || \"robotics bridge command\"),\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n worldState: payload?.worldState || {},\n deviceState: payload?.deviceState || {},\n intent: \"robotics-mechanics\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-bridge\",\n eventType: \"robotics-command\",\n text: `Bridge -> ${String(result.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { directive: result.directive, agentId: payload?.agentId || \"\" }\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"robotics_bridge_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/planner/graph\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const graph = buildTaskActionGraph(String(payload?.task || \"\"), payload?.context || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"planner-graph\",\n text: `Planner graph built: ${graph.taskTitle} nodes=${graph.nodes.length}`,\n roomId: \"studio-main\",\n meta: { taskTitle: graph.taskTitle, nodeCount: graph.nodes.length }\n });\n sendJson(res, 200, { ok: true, graph });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"planner_graph_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerSocialChannel(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, channel: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_channel_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listSocialChannels(auth.uid, 100);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/broadcast\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const text = String(payload?.text || \"\").slice(0, 1200);\n const channels = await listSocialChannels(auth.uid, 100);\n const enabled = channels.filter((c) => c.status === \"enabled\");\n const deliveries = [];\n for (const c of enabled) {\n const out = await deliverSocialWithRetry(c, text);\n deliveries.push({ id: c.id, platform: c.platform, ...out });\n }\n const okCount = deliveries.filter((d) => d.ok).length;\n await addTranscript(auth.uid, {\n source: \"social-layer\",\n eventType: \"social-broadcast\",\n text: `Broadcast delivered ${okCount}/${enabled.length} channel(s): ${text.slice(0, 240)}`,\n roomId: \"studio-main\",\n meta: { deliveries }\n });\n sendJson(res, 200, { ok: true, sentChannels: okCount, totalChannels: enabled.length, deliveries });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_broadcast_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/ethics/programs\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, ...buildEthicsPrograms() });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands/dispatch\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const all = await listCommands(auth.uid, 200);\n const queued = all.filter((c) => String(c.status || \"queued\") === \"queued\").slice(0, 50);\n const devices = await listDevices(auth.uid, 500);\n const byId = new Map(devices.map((d) => [d.id, d]));\n const results = [];\n for (const cmd of queued) {\n const dev = byId.get(cmd.deviceId);\n if (!dev) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: \"device_not_found\" });\n continue;\n }\n try {\n const dispatched = await dispatchCommandToAdapter(dev, cmd);\n await updateCommandStatus(auth.uid, cmd.id, \"sent\");\n results.push({ id: cmd.id, ok: true, adapter: dispatched.adapter, via: dispatched.via });\n } catch (error) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: String(error?.message || \"dispatch_failed\") });\n }\n }\n await addTranscript(auth.uid, {\n source: \"robotics-command-dispatch\",\n eventType: \"command-dispatch-batch\",\n text: `Dispatch batch size=${results.length} ok=${results.filter((x) => x.ok).length}`,\n roomId: \"studio-main\",\n meta: { results: results.slice(0, 30) }\n });\n sendJson(res, 200, { ok: true, processed: results.length, results });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"dispatch_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/pdf/jobs/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n const job = await getPdfJob(auth.uid, id);\n if (!job) return sendJson(res, 404, { ok: false, error: \"pdf_job_not_found\" });\n sendJson(res, 200, { ok: true, job });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const row = await appendMemory({\n scope,\n id,\n text: payload?.text,\n tags: payload?.tags || [],\n importance: payload?.importance,\n kind: payload?.kind || \"episodic\",\n meta: { uid: auth.uid, ...(payload?.meta || {}) }\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.externalTruth) {\n try {\n if (process.env.RHIZOH_EXTERNAL_TRUTH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_truth:${ip}`, RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n sendJson(res, 200, buildRhizohExternalGroundTruthPayload(req));\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.productOutcome) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const rawBody = await readHttpBodyText(req, 64 * 1024);\n const body = rawBody.trim() ? safeJsonParse(rawBody) : null;\n if (!body || typeof body !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"invalid_json_payload\" });\n return;\n }\n const sigHdr = String(req.headers?.[\"x-rhizoh-outcome-signature\"] || \"\").trim();\n const hmacV = verifyRhizohOutcomeHmac(rawBody, sigHdr);\n if (!hmacV.ok) {\n sendJson(res, 401, { ok: false, error: hmacV.error || \"outcome_hmac_invalid\" });\n return;\n }\n const srcTokHdr = String(req.headers?.[\"x-rhizoh-outcome-source-token\"] || \"\").trim();\n const srcV = verifyRhizohOutcomeSourceToken(body.source, srcTokHdr);\n if (!srcV.ok) {\n sendJson(res, 401, { ok: false, error: srcV.error || \"outcome_source_token_invalid\" });\n return;\n }\n const subjectKey = String(body.subjectKey || \"\").trim().slice(0, 128);\n if (\n subjectKey &&\n !checkHttpRateLimit(\n `rhizoh_product_outcome_subj:${subjectKey}`,\n RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN,\n 60_000\n )\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_subject\" });\n return;\n }\n const result = ingestRhizohProductOutcomeHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n schemaVersion: \"1.1.0\",\n aggregate: result.aggregate,\n integrity: result.integrity\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"outcome_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.productOutcomeAggregate) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome_agg:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const u = new URL(String(req.url || \"/\"), \"http://localhost\");\n const cohortId = u.searchParams.get(\"cohortId\") || \"\";\n const decisionFingerprint = u.searchParams.get(\"decisionFingerprint\") || \"\";\n const limit = u.searchParams.get(\"limit\") || \"50\";\n const rows = listRhizohProductOutcomeAggregates({\n cohortId: cohortId || undefined,\n decisionFingerprint: decisionFingerprint || undefined,\n limit: Number(limit) || 50\n });\n sendJson(res, 200, { ok: true, schemaVersion: \"1.1.0\", rows });\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.externalLossBatch) {\n try {\n if (process.env.RHIZOH_EXTERNAL_LOSS_BATCH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_loss_batch:${ip}`, RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const result = ingestRhizohExternalLossBatchHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, { ok: true, accepted: result.accepted ?? 0 });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"batch_failed\") });\n }\n return;\n }\n\n function moderationAdminOk(req) {\n const expected = String(process.env.CASTLE_MODERATION_ADMIN_KEY || \"\").trim();\n if (!expected) return false;\n const got = String(req.headers[\"x-castle-moderation-key\"] || \"\").trim();\n return got.length > 0 && got === expected;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.opsAbuseReport) {\n try {\n const body = await readHttpJson(req, 32 * 1024);\n const auth = await resolveHttpUser(req);\n const entry = submitAbuseReportV0({\n reporterUid: auth.ok ? auth.uid : null,\n targetUid: body?.targetUid ?? null,\n category: body?.category,\n detail: body?.detail,\n traceId: body?.traceId ?? null\n });\n sendJson(res, 202, { ok: true, reportId: entry.id, status: entry.status });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsModerationQueue) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n queue: listModerationQueueV0(100),\n softBlocked: listSoftBlockedUsersV0()\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-block\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const out = softBlockUserV0(body?.uid, body?.reason);\n sendJson(res, out.ok ? 200 : 400, out);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-unblock\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n sendJson(res, 200, removeSoftBlockV0(body?.uid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsAgentSnapshots) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n const limit = Number(new URL(req.url || \"\", \"http://local\").searchParams.get(\"limit\") || 50);\n sendJson(res, 200, { ok: true, snapshots: listRecentAgentSnapshotsV0(limit) });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.academicObservatoryExport) {\n const auth = await resolveHttpUser(req);\n const out = handleAcademicObservatoryExportGetV0(req, auth);\n sendJson(res, out.status, out.body);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsHardeningStatus) {\n const auth = await resolveHttpUser(req);\n const principal = auth.ok ? `uid:${auth.uid}` : null;\n const gclSnap = await getGlobalCostLedgerSnapshotV0(principal);\n const economicStrategy = evaluateEconomicStrategyL3V0(gclSnap);\n const hardeningTenantId = resolveHardeningTenantIdV0(auth, req);\n const unifiedState = await buildUnifiedStateNarrativeV0({\n tenantId: hardeningTenantId ?? undefined,\n platformScope: !hardeningTenantId,\n principal\n });\n sendJson(res, 200, {\n ok: true,\n schema: \"rhizoh.ops.hardening_status.v0\",\n containment: readAgentContainmentConfigV0(),\n cost: readCostContainmentConfigV0(),\n gcl: {\n backend: await getGclBackendStatusV0(),\n snapshot: gclSnap\n },\n economicStrategyL3: economicStrategy,\n phasedRollout: await getPhasedRolloutStatsV0(),\n tier: readPhasedRolloutTierV0(),\n principalCost: auth.ok ? await getCostContainmentStatsV0(principal) : null,\n stressTaxonomy: verifyStressTaxonomyCoverageV0(),\n stressConflictResolution: verifyStressConflictResolutionV0(),\n resolutionStabilityEnvelope: verifyResolutionStabilityEnvelopeV0(),\n unifiedState: {\n tenantScope: {\n tenantId: unifiedState.tenantScope?.tenantId,\n scope: unifiedState.tenantScope?.scope,\n noGlobalDerivedState: unifiedState.tenantScope?.noGlobalDerivedState\n },\n narrativeFingerprint: unifiedState.narrativeFingerprint,\n screenshotScopeWatermark: unifiedState.screenshotScopeWatermark,\n interpretationSafetyContract: unifiedState.interpretationSafetyContract,\n interpretationUxContract: {\n schema: unifiedState.interpretationUxContract?.schema,\n layerOrder: unifiedState.interpretationUxContract?.layerOrder,\n narrativeNeverVisuallyDominant: unifiedState.interpretationUxContract?.narrativeNeverVisuallyDominant\n },\n governance: {\n narrativeIsNotDecisionLayer: unifiedState.governance.narrativeIsNotDecisionLayer,\n displayOrder: unifiedState.governance.displayOrder,\n trustPosture: unifiedState.governance.trustPosture,\n interpretationUxContract: unifiedState.governance.interpretationUxContract?.schema\n },\n humanOps: {\n decisionTier: unifiedState.humanOps?.humanDecisionScaling?.tier,\n decisionOwner: unifiedState.humanOps?.humanDecisionScaling?.decisionOwner,\n opsRouteId: unifiedState.humanOps?.humanDecisionOpsRunbook?.routing?.routeId,\n slaAckMinutes: unifiedState.humanOps?.humanDecisionOpsRunbook?.sla?.ackMinutes,\n uiDisclaimerRequired: unifiedState.humanOps?.narrativeUiSafety?.displayRules?.requireDisclaimerBanner,\n misreadHighResidual: unifiedState.humanOps?.misreadSimulation?.highResidualCount,\n propagationHighResidual: unifiedState.humanOps?.socialPropagationSimulation?.highResidualCount,\n worstWatermarkSurvivability:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.worstWatermarkSurvivability,\n dominantDistortionSource:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.dominantDistortionSource\n },\n culturalRisk: {\n perceptualRiskClass: unifiedState.culturalRisk?.perceptualRiskClass,\n trustFork: unifiedState.culturalRisk?.trustDynamics?.fork,\n trustDecayScore: unifiedState.culturalRisk?.trustDynamics?.scores?.trustDecay,\n mythologyScore: unifiedState.culturalRisk?.trustDynamics?.scores?.mythology,\n confidenceDetachmentExposure:\n unifiedState.culturalRisk?.trustDynamics?.axes?.confidenceDetachmentExposure\n },\n appliedSystemsLayer: {\n schema: unifiedState.appliedSystemsLayer?.schema,\n researchOutputType: unifiedState.appliedSystemsLayer?.researchGovernance?.outputType,\n roboticsBlockActuation: unifiedState.appliedSystemsLayer?.roboticsGrounding?.blockActuation,\n spiralRumorCount:\n unifiedState.appliedSystemsLayer?.spiralMMOGameKernel?.gameLayer?.rumors?.length\n },\n actionSemanticGovernance: {\n schema: unifiedState.actionSemanticGovernance?.schema,\n highRiskActionCount: unifiedState.actionSemanticGovernance?.highRiskActionCount,\n loopClosed: unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.loopClosed,\n authorityCollapseMitigated:\n unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.risks?.authorityCollapse\n ?.mitigated\n },\n actionContextResolution: {\n schema: unifiedState.actionContextResolution?.schema,\n contextFingerprint: unifiedState.actionContextResolution?.contextFingerprint?.fingerprintHash,\n domainCollision: unifiedState.actionContextResolution?.domainCollision?.detected,\n deployAgentSpiralBlocked:\n unifiedState.actionContextResolution?.actionOverloadingRisk?.deployAgentExample\n ?.spiral_mmo === \"blocked\",\n roboticsActuationEligible:\n unifiedState.actionContextResolution?.roboticsActuationCorrectness?.actuationEligible\n },\n epistemicCoherence: {\n schema: unifiedState.epistemicCoherence?.schema,\n verdict: unifiedState.epistemicCoherence?.systemCoherence?.verdict,\n coherenceScore: unifiedState.epistemicCoherence?.systemCoherence?.score,\n contradictionCount: unifiedState.epistemicCoherence?.crossLayerContradictions?.count,\n operatorDecision: unifiedState.epistemicCoherence?.uxCompression?.operatorDecision,\n headlineTr: unifiedState.epistemicCoherence?.uxCompression?.headline?.tr,\n falseSafetyWarning: Boolean(\n unifiedState.epistemicCoherence?.uxCompression?.falseSafetyWarning\n )\n },\n coherenceAuthorityBoundary: {\n schema: unifiedState.coherenceAuthorityBoundary?.schema,\n bindingValid: unifiedState.coherenceAuthorityBoundary?.eclAuthorityBinding?.valid,\n canExecute: unifiedState.coherenceAuthorityBoundary?.contract?.can_execute,\n mandatoryBannerTr:\n unifiedState.coherenceAuthorityBoundary?.disclaimers?.mandatoryBanner?.tr\n },\n decisionLatencyGovernance: {\n schema: unifiedState.decisionLatencyGovernance?.schema,\n latencyTier: unifiedState.decisionLatencyGovernance?.latencyTier,\n inflationScore: unifiedState.decisionLatencyGovernance?.latencyInflation?.score,\n fastPathEligible: unifiedState.decisionLatencyGovernance?.fastPath?.eligible,\n primaryActionId: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.primaryActionId,\n ackSlaMinutes: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.ackSlaMinutes,\n certaintyCap: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.certaintyCap,\n uncertaintyItems:\n unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.uncertaintyEnvelope?.itemCount\n },\n decisionPacketUncertaintyBoundary: {\n schema: unifiedState.decisionPacketUncertaintyBoundary?.schema,\n invariantValid: unifiedState.decisionPacketUncertaintyBoundary?.invariantCheck?.valid,\n centralInvariant: unifiedState.decisionPacketUncertaintyBoundary?.centralInvariant\n },\n epistemicDecisionPacing: {\n schema: unifiedState.epistemicDecisionPacing?.schema,\n queueSaturationTier:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation?.tier,\n operatorProcessingLatency:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation\n ?.operatorProcessingLatency,\n postureWindowCount:\n unifiedState.epistemicDecisionPacing?.temporaryStaticPostureWindows\n ?.temporaryStaticPostureWindows?.windowCount,\n executionBoundariesValid:\n unifiedState.epistemicDecisionPacing?.executionBoundaries?.valid,\n grantsExecutionApproval: unifiedState.epistemicDecisionPacing?.grantsExecutionApproval\n },\n epistemicTemporalCoherence: {\n schema: unifiedState.epistemicTemporalCoherence?.schema,\n allInvariantsValid: unifiedState.epistemicTemporalCoherence?.allInvariantsValid,\n temporalAlignmentValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.temporalAlignment?.valid,\n crossWindowGuardValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard?.valid,\n delayedTruthValid: unifiedState.epistemicTemporalCoherence?.invariants?.delayedTruthRule?.valid,\n reconciliationRequired:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard\n ?.reconciliation?.required\n },\n realityDriftObserver: {\n schema: unifiedState.realityDriftObserver?.schema,\n driftActive: unifiedState.realityDriftObserver?.driftObserverSummary?.active,\n misapprehensionShapeCount:\n unifiedState.realityDriftObserver?.misapprehensionShapeCatalog?.shapeCount,\n criticalShapeCount:\n unifiedState.realityDriftObserver?.driftObserverSummary?.criticalShapeCount,\n roboticsMismatchActive:\n unifiedState.realityDriftObserver?.roboticsFeedbackMismatch?.active\n },\n driftCausality: {\n schema: unifiedState.driftCausality?.schema,\n shapesWithCausalExplanation: unifiedState.driftCausality?.rdolLinkage?.shapesWithCausalExplanation,\n activeDomainCount: unifiedState.driftCausality?.causalitySummary?.activeDomainCount,\n propagationCausalityActive: unifiedState.driftCausality?.causalDomains?.propagation?.active\n },\n productReadiness: unifiedState.productReadiness,\n stateLayers: {\n raw: unifiedState.stateLayers.raw,\n derived: {\n systemState: unifiedState.stateLayers.derived.systemState,\n narrative: unifiedState.stateLayers.derived.narrative,\n validation: unifiedState.stateLayers.derived.validation\n },\n policy: {\n binding: unifiedState.stateLayers.policy.binding,\n decisionOwner: unifiedState.stateLayers.policy.decisionOwner,\n prohibitedActions: unifiedState.stateLayers.policy.prohibitedActions\n }\n },\n systemState: unifiedState.systemState,\n interpretation: unifiedState.interpretation,\n validation: {\n trustPosture: unifiedState.validation.trustPosture,\n adjustedConfidence: unifiedState.validation.adjustedConfidence,\n agreement: unifiedState.validation.confidenceDecomposition.agreement,\n divergenceScore: unifiedState.validation.divergence.divergenceScore,\n validated: unifiedState.validation.divergence.validated,\n uncertaintyTags: unifiedState.validation.uncertainty.tags.map((t) => t.id),\n flags: unifiedState.validation.divergence.flags.map((f) => f.id)\n },\n compression: unifiedState.compression\n }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 6 * 1024 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (\n !checkHttpRateLimit(`rhizoh_voice_transcribe:${rlKey}`, RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN, 60_000)\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const voiceEnv = rhizohVoiceTranscribeEnvV3();\n if (!voiceEnv.any) {\n sendJson(res, 503, {\n ok: false,\n error: \"voice_asr_not_configured\",\n env: voiceEnv\n });\n return;\n }\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n const result = await runRhizohVoiceTranscribeV3(\n applyLanguagePropagationToVoiceBodyV1(payload, langProp)\n );\n sendJson(res, result.ok ? 200 : 422, {\n ...result,\n traceId: String(payload?.traceId || \"\"),\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n atMs: Date.now()\n });\n } catch (error) {\n sendJson(res, 500, {\n ok: false,\n error: \"voice_transcribe_failed\",\n detail: String(error?.message || error),\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(\"/rhizoh/llm/task/\")) {\n try {\n const rawTaskId = decodeURIComponent(pathname.slice(\"/rhizoh/llm/task/\".length).split(\"/\")[0] || \"\");\n const taskId = String(rawTaskId || \"\").toLowerCase() === \"last\" ? getLatestLlmWorkerTaskIdV0() : rawTaskId;\n const snap = taskId ? getLlmWorkerTaskSnapshotV0(taskId) : null;\n if (!snap) {\n sendJson(res, 404, {\n ok: false,\n error: \"llm_task_not_found\",\n taskId: rawTaskId,\n status: \"missing\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n if (snap.status === \"processing\") {\n sendJson(res, 202, {\n ok: true,\n taskId: snap.taskId,\n status: \"processing\",\n createdAt: snap.createdAt,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n // Terminal states (completed/failed) always HTTP 200 — poll must not look like server fault (500).\n sendJson(res, 200, {\n ...(snap.body || { ok: false, error: \"llm_task_empty\", taskId: snap.taskId }),\n taskId: snap.taskId,\n status: snap.status === \"completed\" ? \"completed\" : snap.status,\n terminal: true,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n } catch (error) {\n console.error(\"[llm-task-poll] handler failed:\", error);\n sendJson(res, 200, {\n ok: false,\n error: \"llm_task_poll_handler_failed\",\n detail: String(error?.message || error),\n status: \"failed\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === rhizohRuntime.routes.rhizohLlm) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 128 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n logRhizohHealth(\"gateway_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: String(payload?.traceId || \"\"),\n auth: auth.ok ? \"ok\" : \"anon\",\n ip\n });\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (!checkHttpRateLimit(`rhizoh_llm:${rlKey}`, RL_RHIZOH_LLM_PER_MIN, 60_000)) {\n const rlTaxonomy = classifyStressResponseV0({ code: \"rate_limit_exceeded\" });\n return sendJson(res, 429, {\n ok: false,\n error: \"rate_limit_exceeded\",\n reply: rlTaxonomy.userMessageTr || \"İstek sınırı aşıldı. Kısa süre sonra tekrar deneyin.\",\n stressClass: rlTaxonomy.stressClass,\n responseAction: rlTaxonomy.responseAction,\n stressMatrix: rlTaxonomy.matrix,\n stressInterpretable: rlTaxonomy.interpretable,\n stressConfidence: rlTaxonomy.stressConfidence,\n actionConfidence: rlTaxonomy.actionConfidence,\n actionSoftened: rlTaxonomy.actionSoftened,\n responseActionStrict: rlTaxonomy.responseActionStrict,\n actionInterpretable: rlTaxonomy.actionInterpretable,\n conflictResolution: rlTaxonomy.conflictResolution,\n directive: \"NONE\"\n });\n }\n\n const requireExplicit = process.env.CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE === \"1\";\n const keyMode = normalizeClientLlmKeySource(payload?.llmKeySource ?? payload?.keySource);\n if (keyMode === null) {\n return sendJson(res, 400, {\n ok: false,\n error: \"invalid_llm_key_source\",\n reply: \"llmKeySource geçersiz. Kullanın: env | user_connection | auto\",\n directive: \"NONE\"\n });\n }\n if (requireExplicit && keyMode === \"auto\") {\n return sendJson(res, 400, {\n ok: false,\n error: \"llm_key_source_required\",\n reply: \"Sunucu CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE=1: istekte llmKeySource (env veya user_connection) zorunlu.\",\n directive: \"NONE\"\n });\n }\n\n const conn = auth.ok ? await resolveConnection(auth.uid, String(payload?.connectionId || \"\")) : null;\n\n if (keyMode === \"user_connection\") {\n if (!auth.ok) {\n return sendJson(res, 401, {\n ok: false,\n error: \"llm_auth_required_for_user_connection\",\n reply: \"Kayıtlı LLM bağlantısı için giriş gerekli.\",\n directive: \"NONE\"\n });\n }\n if (!conn?.apiKey) {\n return sendJson(res, 400, {\n ok: false,\n error: \"user_llm_connection_required\",\n reply: \"Geçerli bir LLM bağlantısı yok; Studio’da bağlantı ekleyin veya llmKeySource=env kullanın.\",\n directive: \"NONE\"\n });\n }\n }\n\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n let safePayload = applyLanguagePropagationToLlmPayloadV1({ ...(payload || {}) }, langProp);\n const trimOut = trimRhizohLlmGatewayPayloadV1(safePayload);\n if (trimOut.meta.trimmed) {\n logRhizohHealth(\"llm_payload_trimmed\", {\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n bytesBefore: trimOut.meta.bytesBefore,\n bytesAfter: trimOut.meta.bytesAfter\n });\n }\n safePayload = trimOut.payload;\n delete safePayload.apiKey;\n delete safePayload.llmKeySource;\n delete safePayload.keySource;\n\n const llmMessage = String(\n safePayload?.message || safePayload?.text || payload?.message || payload?.text || \"\"\n ).trim();\n if (!llmMessage) {\n return sendJson(res, 400, {\n ok: false,\n error: \"message_required\",\n reply: \"Mesaj metni gerekli (message alanı boş).\",\n directive: \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n if (!safePayload.message) {\n safePayload = { ...safePayload, message: llmMessage };\n }\n\n let resolvedProvider = payload?.provider;\n let resolvedModel = payload?.model;\n let connApiKey = \"\";\n\n ({\n resolvedProvider,\n resolvedModel,\n connApiKey\n } = resolveServerLlmProviderV0(keyMode, conn, payload));\n\n const turnInput = {\n safePayload,\n auth,\n keyMode,\n conn,\n resolvedProvider,\n resolvedModel,\n connApiKey\n };\n const turnMeta = {\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n langEcho: buildRhizohLanguagePropagationEchoV1(langProp)\n };\n\n if (\n isLlmWorkerEnabledV0() &&\n isLlmWorkerAsyncEnabledV0() &&\n String(req.headers[\"x-rhizoh-llm-sync\"] || \"\").trim() !== \"1\"\n ) {\n const taskId = enqueueLlmWorkerTaskAsyncV0(turnInput, turnMeta);\n logRhizohHealth(\"llm_worker_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n taskId,\n traceId: String(payload?.traceId || taskId),\n keyMode,\n async: true\n });\n sendJson(res, 202, {\n ok: true,\n taskId,\n status: \"processing\",\n traceId: String(payload?.traceId || taskId),\n pollPath: `/rhizoh/llm/task/${taskId}`,\n gatewayPid: process.pid,\n ...turnMeta.langEcho\n });\n return;\n }\n\n const { result, traceId, turnLatencyMs, spinePhases, sampledTrace } = isLlmWorkerEnabledV0()\n ? await runLlmWorkerTaskV0(turnInput, turnMeta)\n : await rhizohGatewayTurn(turnInput);\n logRhizohHealth(\"llm_response\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId,\n turnLatencyMs: Number(turnLatencyMs || 0),\n keyMode\n });\n sendJson(res, 200, {\n ...result,\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n traceId,\n turnLatencyMs,\n ...(spinePhases ? { spinePhases } : {}),\n ...(sampledTrace !== undefined ? { sampledTrace } : {})\n });\n } catch (error) {\n const msg = String(error?.message || \"\");\n const code = error?.code || \"\";\n let status = 500;\n if (msg === \"payload_too_large\") status = 413;\n else if (msg === \"rate_limit_exceeded\") status = 429;\n else if (\n code === \"cost_hard_limit\" ||\n code === \"phased_rollout_capacity\" ||\n error?.containment === true ||\n String(code).startsWith(\"agent_\")\n ) {\n status = 429;\n } else if (code === \"user_soft_blocked\" || code === \"prompt_abuse_detected\") status = 403;\n else if (\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(code) ||\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(msg) ||\n msg.includes(\"missing_api_key_for_\")\n )\n status = 400;\n let rhizohFailureKind = \"provider_error\";\n if (msg === \"rate_limit_exceeded\" || status === 429) rhizohFailureKind = \"rate_limit\";\n else if (status === 403) rhizohFailureKind = \"policy_block\";\n else if (status === 400 || status === 413) rhizohFailureKind = \"client_config\";\n else if (msg.startsWith(\"provider_http_\")) {\n const st = Number(msg.replace(\"provider_http_\", \"\"));\n if (st === 408 || st === 504) rhizohFailureKind = \"timeout\";\n else rhizohFailureKind = \"provider_error\";\n } else if (/timeout|timed out|ETIMEDOUT/i.test(msg)) rhizohFailureKind = \"timeout\";\n\n const providerHttpMatch = msg.match(/^provider_http_(\\d{3})$/);\n const stressTaxonomy =\n error?.stressClass != null\n ? {\n stressClass: error.stressClass,\n responseAction: error.responseAction,\n stressMatrix: error.stressMatrix,\n stressInterpretable: error.stressInterpretable !== false,\n stressConfidence: error.stressConfidence,\n actionConfidence: error.actionConfidence,\n actionSoftened: error.actionSoftened,\n responseActionStrict: error.responseActionStrict,\n actionInterpretable: error.actionInterpretable,\n conflictResolution: error.conflictResolution,\n stressSecondary: error.stressSecondary,\n responseActions: error.responseActions\n }\n : (() => {\n const t = classifyStressResponseV0({\n code: code || msg,\n rhizohFailureKind,\n providerHttpStatus: providerHttpMatch\n ? Number(providerHttpMatch[1])\n : null\n });\n return {\n stressClass: t.stressClass,\n responseAction: t.responseAction,\n stressMatrix: t.matrix,\n stressInterpretable: t.interpretable,\n stressConfidence: t.stressConfidence,\n actionConfidence: t.actionConfidence,\n actionSoftened: t.actionSoftened,\n responseActionStrict: t.responseActionStrict,\n actionInterpretable: t.actionInterpretable,\n conflictResolution: t.conflictResolution,\n stressSecondary: t.stressSecondary,\n responseActions: t.responseActions\n };\n })();\n sendJson(res, status, {\n ok: false,\n rhizohFailureKind,\n ...(providerHttpMatch ? { providerHttpStatus: Number(providerHttpMatch[1]) } : {}),\n error: code || msg || \"rhizoh_llm_failed\",\n detail: msg || code || undefined,\n reply:\n error?.reply ||\n (status === 500 ? \"Rhizoh bağlantısı geçici olarak kesildi.\" : msg),\n directive: error?.directive || \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n ...stressTaxonomy\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicSeal) {\n try {\n if (!EPISTEMIC_SEAL_SECRET) {\n sendJson(res, 503, { ok: false, error: \"epistemic_seal_disabled\" });\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`epistemic_seal:${ip}`, RL_EPISTEMIC_SEAL_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const truth = body.truth_contract;\n if (!truth || typeof truth !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"truth_contract_required\" });\n return;\n }\n const sealBody = {\n truth_contract: truth,\n runtime_hash: String(body.runtime_hash || \"\"),\n model_route: {\n provider: body.model_route?.provider ?? null,\n model: body.model_route?.model ?? null\n },\n memory_digest: String(body.memory_digest || \"\"),\n world_snapshot_hash: String(body.world_snapshot_hash || \"\"),\n timestamp: Number(body.timestamp) || Date.now()\n };\n const canonical = canonicalEpistemicSealString(sealBody);\n const { hash, signature } = hashAndSignEpistemicSeal(canonical, EPISTEMIC_SEAL_SECRET);\n recordGenesisEpistemicSealIssued(hash);\n const attestation = {\n schema: EPISTEMIC_SEAL_SCHEMA,\n algorithm: \"SHA-256+HMAC-SHA256\",\n keyHint: process.env.CASTLE_EPISTEMIC_SEAL_SECRET ? \"CASTLE_EPISTEMIC_SEAL_SECRET\" : \"CASTLE_GATEWAY_TOKEN\",\n issuedAt: Date.now(),\n gateway: { service: \"castle-gateway\", listenPort: PORT }\n };\n sendJson(res, 200, {\n ok: true,\n hash,\n signature,\n attestation,\n canonicalBytes: Buffer.byteLength(canonical, \"utf8\")\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"seal_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicLogsBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`epistemic_logs:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 120) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const persisted = await persistEpistemicLedgerBatch(auth.uid, entries);\n recordGenesisEpistemicLedgerPersisted(persisted.persisted);\n // Internal cognition layer: forecast stays gateway/ledger-only (no client exposure).\n try {\n await persistEpistemicForecastBatch(auth.uid, persisted.normalized || []);\n } catch {\n /* forecast failures must not break primary ledger ingest */\n }\n sendJson(res, 200, {\n ok: true,\n persisted: persisted.persisted,\n mode: persisted.mode,\n latest: persisted.normalized?.[persisted.normalized.length - 1] || null\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"epistemic_log_ingest_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.authorityLedgerBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`authority_ledger:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 80) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const witnessed = persistAuthorityLedgerWitnessBatchV1(auth.uid, entries, EPISTEMIC_SEAL_SECRET);\n if (witnessed.witnessed > 0) {\n recordGenesisEpistemicLedgerPersisted(witnessed.witnessed);\n const clientSeal = witnessed.lastWitness?.clientSealHash;\n if (clientSeal) recordGenesisEpistemicSealIssued(clientSeal);\n }\n sendJson(res, 200, {\n ok: true,\n witnessed: witnessed.witnessed,\n quarantined: witnessed.quarantined,\n chainHeight: witnessed.chainHeight,\n chainHead: witnessed.chainHead,\n lastWitness: witnessed.lastWitness,\n results: witnessed.results\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"authority_ledger_witness_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.authorityLedgerReplay) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const replayUrl = new URL(String(req.url || \"\"), \"http://localhost\");\n const epochId = String(replayUrl.searchParams.get(\"epochId\") || \"\").trim() || undefined;\n const snapshot = getAuthorityLedgerWitnessSnapshotV1(auth.uid, epochId);\n const replay = replayAuthorityLedgerWitnessChainV1(auth.uid, epochId);\n sendJson(res, 200, {\n ok: true,\n snapshot,\n replay,\n schema: \"castle.rhizoh.authority_ledger_witness.v1.replay_surface\"\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"authority_ledger_replay_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.authorityEpochMerge) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`authority_epoch_merge:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const merged = assimilateAuthorityEpochMergeV1(auth.uid, body);\n if (!merged.ok) {\n sendJson(res, 400, merged);\n return;\n }\n sendJson(res, 200, {\n ok: true,\n mergeEvent: merged.mergeEvent,\n recentMerges: merged.recentMerges\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"authority_epoch_merge_failed\") });\n }\n return;\n }\n\n sendJson(res, 404, { ok: false, error: \"not_found\" });\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\nspiralWssForGenesis = wss;\nconst orchestrator = createOrchestrator();\nconst clientStats = new Map();\nlet broadcasterClientId = null;\nconst spiralState = {\n rooms: [{ id: \"room-default\", name: \"Spiral Default Arena\" }],\n activeByClient: new Map(),\n characters: new Map() // id -> {id,name,role,roomId,source}\n};\n\n/** Castle social rooms — keyed by `castleRoomKey` from {@link WS_MESSAGE.CASTLE_SOCIAL_PULSE}. */\nconst castleSocialRoomState = new Map();\nconst CASTLE_SOCIAL_MEMBER_TTL_MS = 90_000;\nconst CASTLE_SOCIAL_MAX_PULSE_BYTES = 8192;\nconst RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES = 5 * 1024 * 1024;\nconst RHIZOH_VOICE_LIVE_MAX_CHUNKS = 96;\n\n/**\n * Voice Engine v3 live media lane.\n * Client streams MediaRecorder chunks over the existing gateway WS; STOP reuses the\n * gateway ASR runner so orchestration remains the single authority downstream.\n */\nconst rhizohVoiceLiveSessions = new WeakMap();\n\nfunction appendRhizohVoiceLiveEventV0(session, type, payloadRefSeed = \"\") {\n if (!session) return null;\n session.eventSeq = (Number(session.eventSeq) || 0) + 1;\n const event = createRhizohImmutableEventV0({\n type,\n sessionId: session.sessionId,\n traceId: session.traceId,\n actorSeed: session.sessionId || session.traceId,\n eventSeq: session.eventSeq,\n payloadRef: createRhizohPayloadRefV0(payloadRefSeed || `${session.sessionId}:${type}:${session.eventSeq}`)\n });\n session.events = [...(session.events || []), event].slice(-64);\n return event;\n}\n\nfunction sendRhizohVoiceLiveEnvelopeV0(socket, type, payload) {\n try {\n if (socket?.readyState === 1) socket.send(JSON.stringify(createEnvelope(type, payload)));\n } catch {\n /* noop */\n }\n}\n\nfunction decodeRhizohVoiceLiveChunkV0(payload = {}) {\n const b64 = String(payload.audioBase64 || \"\").trim();\n if (!b64) return { ok: false, error: \"audio_base64_required\" };\n try {\n const buf = Buffer.from(b64, \"base64\");\n if (!buf.length) return { ok: false, error: \"audio_decode_empty\" };\n return { ok: true, buffer: buf };\n } catch {\n return { ok: false, error: \"audio_base64_invalid\" };\n }\n}\n\nfunction handleRhizohVoiceLiveStartV0(socket, payload = {}) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const sessionId = String(p.sessionId || `live_${Date.now().toString(36)}`).slice(0, 96);\n rhizohVoiceLiveSessions.set(socket, {\n sessionId,\n traceId: String(p.traceId || \"\").slice(0, 128),\n mimeType: String(p.mimeType || \"audio/webm\").slice(0, 80),\n languageCode: String(p.languageCode || \"tr-TR\").slice(0, 32),\n path: String(p.path || \"both\").slice(0, 24),\n chunks: [],\n chunkIndexes: [],\n totalBytes: 0,\n nextChunkIndex: 1,\n eventSeq: 0,\n events: [],\n startedAtMs: Date.now()\n });\n appendRhizohVoiceLiveEventV0(rhizohVoiceLiveSessions.get(socket), \"VOICE_LIVE_SESSION_STARTED\", `${sessionId}:live_session`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_START, {\n ok: true,\n sessionId,\n schema: \"castle.rhizoh.voice_live.gateway_lane.v0\"\n });\n}\n\nfunction handleRhizohVoiceLiveChunkV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const sequence = validateRhizohVoiceLiveChunkPayloadV0(payload, {\n expectedChunkIndex: session.nextChunkIndex\n });\n if (!sequence.ok) {\n rhizohVoiceLiveSessions.delete(socket);\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:chunk_reject`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: sequence.error,\n expectedChunkIndex: sequence.expectedChunkIndex,\n chunkIndex: sequence.chunkIndex\n });\n return;\n }\n const decoded = decodeRhizohVoiceLiveChunkV0(payload);\n if (!decoded.ok) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: decoded.error\n });\n return;\n }\n if (\n session.chunks.length >= RHIZOH_VOICE_LIVE_MAX_CHUNKS ||\n session.totalBytes + decoded.buffer.length > RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES\n ) {\n rhizohVoiceLiveSessions.delete(socket);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_audio_too_large\"\n });\n return;\n }\n session.chunks.push(decoded.buffer);\n session.chunkIndexes.push(sequence.chunkIndex);\n session.totalBytes += decoded.buffer.length;\n session.nextChunkIndex += 1;\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_CHUNK_ACCEPTED\", `${session.sessionId}:chunk:${sequence.chunkIndex}`);\n}\n\nfunction handleRhizohVoiceLiveStopV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n rhizohVoiceLiveSessions.delete(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const lastChunkIndex = Number(payload?.lastChunkIndex);\n if (Number.isInteger(lastChunkIndex) && lastChunkIndex !== session.chunks.length) {\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:stop_mismatch`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_chunk_count_mismatch\",\n expectedChunkCount: lastChunkIndex,\n receivedChunkCount: session.chunks.length\n });\n return;\n }\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_STOP_RECEIVED\", `${session.sessionId}:stop:${session.chunks.length}`);\n const audio = Buffer.concat(session.chunks);\n if (!audio.length) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"audio_empty\"\n });\n return;\n }\n runRhizohVoiceTranscribeV3({\n path: String(payload?.path || session.path || \"both\"),\n audioBase64: audio.toString(\"base64\"),\n mimeType: String(payload?.mimeType || session.mimeType || \"audio/webm\"),\n languageCode: String(payload?.languageCode || session.languageCode || \"tr-TR\"),\n traceId: String(payload?.traceId || session.traceId || \"\"),\n sessionId: session.sessionId,\n transport: \"gateway_ws_live\"\n })\n .then((result) => {\n appendRhizohVoiceLiveEventV0(session, result?.ok ? \"VOICE_TRANSCRIBE_FINAL\" : \"VOICE_TRANSCRIBE_FAILED\", `${session.sessionId}:final`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_FINAL, {\n ...result,\n sessionId: session.sessionId,\n transportPath: \"gateway_ws_live\",\n bytes: audio.length,\n chunkCount: session.chunks.length,\n chunkSequence: Object.freeze({\n ok: true,\n firstChunkIndex: session.chunkIndexes[0] || null,\n lastChunkIndex: session.chunkIndexes[session.chunkIndexes.length - 1] || null,\n receivedChunkCount: session.chunks.length\n }),\n eventTrace: Object.freeze((session.events || []).slice(-16)),\n recordedMs: Math.max(0, Date.now() - session.startedAtMs)\n });\n })\n .catch((e) => {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_transcribe_failed\",\n detail: String(e?.message || e)\n });\n });\n}\n\nfunction removeCastleSocialClientFromAllRooms(clientId) {\n const cid = String(clientId || \"\");\n if (!cid) return;\n for (const room of castleSocialRoomState.values()) {\n room.members.delete(cid);\n }\n}\n\nfunction buildCastleSocialRoomPayload(roomKey) {\n const room = castleSocialRoomState.get(roomKey);\n if (!room) return null;\n const now = Date.now();\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const roster = [...room.members.values()].map((m) => ({\n userId: m.userId,\n lastMs: m.lastMs,\n ...m.sp\n }));\n const peerFeeds =\n room.walPeers && room.walPeers.size\n ? [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }))\n : [];\n return { castleRoomKey: roomKey, seq: room.seq, roster, peerFeeds, ts: now };\n}\n\nconst CASTLE_WAL_PEER_MAX_FEED_BYTES = 48 * 1024;\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {{ ok: boolean, code?: string, reason?: string }} gate\n */\nfunction rejectWalPeerIngressV0(socket, gate) {\n const code = gate.code || \"wal_peer_rejected\";\n recordWalPeerRejectV0(code);\n recordSubstrateOtelEventV0(\"wal_peer_reject\", { code, reason: String(gate.reason || \"\").slice(0, 120) });\n socket.send(\n JSON.stringify(\n createEnvelope(WS_MESSAGE.ERROR, {\n error: gate.reason || \"WAL peer ingress rejected.\",\n code\n })\n )\n );\n}\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {Record<string, unknown> | null | undefined} walPeerFeed\n * @returns {boolean}\n */\nfunction gateWalPeerIngressV0(socket, walPeerFeed) {\n const authGate = assertWalPeerSocketAuthorityV0(socket);\n if (!authGate.ok) {\n rejectWalPeerIngressV0(socket, authGate);\n return false;\n }\n const sigGate = validateWalPeerFeedSignatureV0(\n walPeerFeed && typeof walPeerFeed === \"object\" ? walPeerFeed : {}\n );\n if (!sigGate.ok) {\n rejectWalPeerIngressV0(socket, sigGate);\n return false;\n }\n return true;\n}\n\nfunction handleCastleWalPeerFeed(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_WAL_PEER_MAX_FEED_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_WAL_PEER_FEED too large.\" })));\n return;\n }\n const walPeerFeed = p.walPeerFeed && typeof p.walPeerFeed === \"object\" ? p.walPeerFeed : {};\n if (!gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const castleRoomKey = String(p.castleRoomKey || \"wal:convergence\").slice(0, 64);\n const castleId = String(p.castleId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId,\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: room.members.get(socket.clientId)?.sp ?? {}\n });\n const peerFeeds = [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }));\n const body = {\n castleRoomKey,\n seq: room.seq,\n peerFeeds,\n ts: now\n };\n const env = createEnvelope(WS_MESSAGE.CASTLE_WAL_PEER_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys(), ...room.walPeers.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction handleCastleSocialPulse(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_SOCIAL_MAX_PULSE_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_SOCIAL_PULSE too large.\" })));\n return;\n }\n const castleRoomKey = String(p.castleRoomKey || \"default\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n const sp = p.socialPulse && typeof p.socialPulse === \"object\" ? p.socialPulse : {};\n const walPeerFeed =\n p.walPeerFeed && typeof p.walPeerFeed === \"object\"\n ? p.walPeerFeed\n : sp.walPeerFeed && typeof sp.walPeerFeed === \"object\"\n ? sp.walPeerFeed\n : null;\n if (walPeerFeed && !gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const spNext = walPeerFeed ? { ...sp, walPeerFeed } : { ...sp };\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: spNext\n });\n if (walPeerFeed) {\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId: String(p.castleId || userId).slice(0, 64),\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n }\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const body = buildCastleSocialRoomPayload(castleRoomKey);\n if (!body) return;\n const env = createEnvelope(WS_MESSAGE.CASTLE_SOCIAL_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcast(msgObj) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcastState() {\n broadcast(\n createEnvelope(WS_MESSAGE.BROADCAST_STATE, {\n broadcasterClientId,\n viewers: [...wss.clients].filter((c) => c.readyState === 1).length\n })\n );\n}\n\nfunction currentSpiralStatePayload() {\n return {\n rooms: spiralState.rooms,\n characters: [...spiralState.characters.values()]\n };\n}\n\nfunction broadcastSpiralState() {\n broadcast(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload()));\n}\n\nfunction ensureRoom(roomId, fallbackName = null) {\n if (!roomId) return spiralState.rooms[0].id;\n const exists = spiralState.rooms.find((r) => r.id === roomId);\n if (exists) return exists.id;\n const created = { id: roomId, name: fallbackName || `Spiral-${roomId.slice(-4)}` };\n spiralState.rooms.push(created);\n return created.id;\n}\n\nfunction syncSpiralCharactersWithWorld(snapshot) {\n const agents = snapshot?.agents || [];\n for (const a of agents) {\n if (!spiralState.characters.has(a.id)) {\n spiralState.characters.set(a.id, {\n id: a.id,\n name: a.id,\n role: \"agent\",\n roomId: spiralState.rooms[0].id,\n source: \"world\"\n });\n }\n }\n}\n\nfunction broadcastExcept(msgObj, exceptSocket) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client !== exceptSocket && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction validateInputFrame(payload, socket) {\n const stats = clientStats.get(socket.clientId) || { lastFrame: 0, windowStart: Date.now(), msgCount: 0 };\n const now = Date.now();\n if (now - stats.windowStart > 5000) {\n stats.windowStart = now;\n stats.msgCount = 0;\n }\n stats.msgCount += 1;\n clientStats.set(socket.clientId, stats);\n if (stats.msgCount > 120) return \"Rate limit exceeded.\";\n\n if (!payload || !Number.isInteger(payload.frame)) return \"frame must be integer.\";\n if (payload.frame <= stats.lastFrame) return \"frame must be monotonic.\";\n const commands = Array.isArray(payload.commands) ? payload.commands : [];\n if (commands.length > 8) return \"Too many commands in one frame.\";\n for (const cmd of commands) {\n if (!cmd || typeof cmd.command !== \"string\") return \"Invalid command format.\";\n if (\n cmd.command !== \"SPAWN_AGENT\" &&\n cmd.command !== \"LIST_CASTLES\" &&\n cmd.command !== \"SPAWN_ENTITY\" &&\n cmd.command !== \"CREATE_CASTLE\"\n ) return \"Unsupported command.\";\n }\n stats.lastFrame = payload.frame;\n clientStats.set(socket.clientId, stats);\n return null;\n}\n\nfunction validateSignalPayload(payload) {\n if (!payload || typeof payload !== \"object\") return \"Invalid signaling payload.\";\n const signalType = payload.signalType;\n if (Object.values(CASTLE_NETWORK_SIGNAL_V0).includes(signalType)) {\n return null;\n }\n if (![\"OFFER\", \"ANSWER\", \"ICE\"].includes(signalType)) return \"Invalid signal type.\";\n if (typeof payload.to !== \"string\" || payload.to.length < 3) return \"Invalid signal destination.\";\n if (signalType !== \"ICE\" && JSON.stringify(payload.sdp || {}).length > 16 * 1024) return \"SDP too large.\";\n if (signalType === \"ICE\" && JSON.stringify(payload.candidate || {}).length > 6 * 1024) return \"ICE too large.\";\n return null;\n}\n\nfunction sanitizeRawMessage(raw) {\n if (!raw) return null;\n const text = raw.toString();\n if (Buffer.byteLength(text, \"utf8\") > MAX_MESSAGE_BYTES) return null;\n return text;\n}\n\nwss.on(\"connection\", async (socket, req) => {\n const origin = req?.headers?.origin || \"\";\n if (!httpCors.isWebSocketOriginAllowed(origin)) {\n socket.close(1008, \"Origin not allowed\");\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const path = req?.url || \"\";\n const query = new URL(`ws://localhost${path}`).searchParams;\n const token = query.get(\"token\");\n if (!token || token !== REQUIRED_GATEWAY_TOKEN) {\n socket.close(1008, \"Invalid gateway token\");\n return;\n }\n }\n if (REQUIRE_AUTH) {\n const result = await verifyClientToken(req);\n if (!result.ok) {\n const isLocal = origin.includes(\"localhost\") || origin.includes(\"127.0.0.1\") || !origin;\n if (!(ALLOW_DEV_ANON && isLocal)) {\n socket.close(1008, result.reason);\n return;\n }\n socket.auth = { ok: true, kind: \"dev-anon\", user: { uid: \"dev-anon\" } };\n } else {\n socket.auth = result;\n }\n }\n\n socket.clientId = `c-${Math.random().toString(36).slice(2, 8)}`;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.HELLO, { message: \"Castle Gateway online.\", clientId: socket.clientId })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.WORLD_SNAPSHOT, orchestrator.worldSnapshot())));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.BROADCAST_STATE, { broadcasterClientId, viewers: [...wss.clients].filter((c) => c.readyState === 1).length })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload())));\n broadcastExcept(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }), socket);\n\n socket.on(\"message\", (raw) => {\n const safeRaw = sanitizeRawMessage(raw);\n if (!safeRaw) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Message too large or invalid.\" })));\n return;\n }\n const parsed = safeJsonParse(safeRaw);\n if (!parsed?.type) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid message.\" })));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND_TEXT) {\n const canonical = parseTextToCommand(parsed.payload?.text);\n if (!canonical) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Unknown command text.\" })));\n return;\n }\n const result = orchestrator.applyCommand(canonical);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.MATCH_SESSION_JOIN) {\n handleMatchSessionJoinV0(socket, parsed, wss);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.MATCH_MOVE) {\n handleMatchMoveAuthorityV0(socket, parsed, wss);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND) {\n const result = orchestrator.applyCommand(parsed.payload);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n }\n\n if (parsed.type === WS_MESSAGE.INPUT_FRAME) {\n const err = validateInputFrame(parsed.payload, socket);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const frame = parsed.payload.frame;\n const commands = Array.isArray(parsed.payload?.commands) ? parsed.payload.commands : [];\n orchestrator.queueInputFrame(socket.clientId, frame, commands);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, queued: true, frame })));\n }\n\n if (parsed.type === WS_MESSAGE.SIGNAL) {\n const payload = parsed.payload || {};\n if (Object.values(CASTLE_NETWORK_SIGNAL_V0).includes(payload.signalType)) {\n const netErr = validateCastleNetworkSignalPayloadV0(socket, payload);\n if (netErr) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: netErr })));\n return;\n }\n handleCastleNetworkSignalV0(socket, payload, wss);\n return;\n }\n const err = validateSignalPayload(payload);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const signalType = payload.signalType;\n const to = payload.to;\n for (const client of wss.clients) {\n if (client.clientId === to && client.readyState === 1) {\n client.send(JSON.stringify(createEnvelope(WS_MESSAGE.SIGNAL, { ...payload, from: socket.clientId })));\n break;\n }\n }\n }\n\n if (parsed.type === WS_MESSAGE.DRONE_TELEMETRY) {\n const payload = parsed.payload || {};\n const json = JSON.stringify(payload);\n if (Buffer.byteLength(json, \"utf8\") > 48 * 1024) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"DRONE_TELEMETRY payload too large.\" })));\n return;\n }\n broadcastExcept(createEnvelope(WS_MESSAGE.DRONE_TELEMETRY, { ...payload, relayFrom: socket.clientId }), socket);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_START) {\n handleRhizohVoiceLiveStartV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_CHUNK) {\n handleRhizohVoiceLiveChunkV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_STOP) {\n handleRhizohVoiceLiveStopV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_SOCIAL_PULSE) {\n handleCastleSocialPulse(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_WAL_PEER_FEED) {\n handleCastleWalPeerFeed(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.OPEN_DATA_QUERY) {\n queryOpenData(parsed.payload)\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: true, result })));\n })\n .catch((e) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: false, error: e.message })));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_QUERY) {\n runRhizohBrainV2({\n ...(parsed.payload || {}),\n uid: socket?.auth?.user?.uid || \"unknown\"\n })\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, result)));\n })\n .catch(async () => {\n const fallback = await runRhizohBrain(parsed.payload || {}).catch(() => ({\n ok: false,\n reply: \"Rhizoh temporary disturbance. Habitat remains stable.\"\n }));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, fallback)));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.BROADCAST_REGISTER) {\n const role = String(parsed.payload?.role || \"\");\n if (role === \"GENESIS_BROADCAST_AGENT\") {\n broadcasterClientId = socket.clientId;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, registered: role })));\n broadcastState();\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.STUDIO_CUE) {\n // Only registered broadcaster can push global cues.\n if (!broadcasterClientId || socket.clientId !== broadcasterClientId) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Only broadcaster agent can emit STUDIO_CUE.\" })));\n return;\n }\n const cue = parsed.payload || {};\n if (typeof cue !== \"object\") {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid STUDIO_CUE payload.\" })));\n return;\n }\n broadcast(createEnvelope(WS_MESSAGE.STUDIO_CUE, cue));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_JOIN_ROOM) {\n const requested = String(parsed.payload?.roomId || \"\").trim();\n const requestedName = String(parsed.payload?.roomName || \"\").trim();\n const roomId = ensureRoom(requested || `room-${Math.random().toString(36).slice(2, 7)}`, requestedName || null);\n spiralState.activeByClient.set(socket.clientId, roomId);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, roomId })));\n broadcastSpiralState();\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_CREATE_CHARACTER) {\n const name = String(parsed.payload?.name || \"\").trim().slice(0, 32) || `AI-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;\n const role = String(parsed.payload?.role || \"agent\").trim().slice(0, 24) || \"agent\";\n const roomId = ensureRoom(\n String(parsed.payload?.roomId || spiralState.activeByClient.get(socket.clientId) || spiralState.rooms[0].id),\n null\n );\n\n // Character creation always maps to an authoritative world agent.\n const result = orchestrator.applyCommand({\n command: COMMAND.SPAWN_AGENT,\n params: { targetCastleId: \"castle-01\" }\n });\n if (!result?.ok || !result?.data?.id) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Character creation failed.\" })));\n return;\n }\n const id = result.data.id;\n spiralState.characters.set(id, { id, name, role, roomId, source: \"spawned\" });\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, characterId: id, roomId })));\n broadcastSpiralState();\n return;\n }\n });\n\n socket.on(\"close\", () => {\n if (socket.clientId === broadcasterClientId) broadcasterClientId = null;\n leaveMatchBroadcastRoomV0(socket);\n clientStats.delete(socket.clientId);\n rhizohVoiceLiveSessions.delete(socket);\n spiralState.activeByClient.delete(socket.clientId);\n removeCastleSocialClientFromAllRooms(socket.clientId);\n removeCastleNetworkClientV0(socket.clientId, wss);\n broadcast(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }));\n broadcastState();\n });\n});\n\nsetInterval(() => {\n void (async () => {\n try {\n const { frame, snapshot, delta, packedDelta } = orchestrator.tick(1 / 20);\n syncSpiralCharactersWithWorld(snapshot);\n broadcast(createEnvelope(WS_MESSAGE.NETWORK_TICK, { frame }));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_TICK, snapshot));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA, delta));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA_PACKED, packedDelta));\n broadcastSpiralState();\n await runAcademyEventTick();\n const executed = await tickAutomations(Date.now());\n for (const a of executed) {\n await addTranscript(a.uid, {\n source: \"planner\",\n eventType: \"automation-executed\",\n text: `Automation executed: ${a.title} (${a.action})`,\n roomId: \"studio-main\",\n meta: { automationId: a.id, action: a.action, deviceId: a.deviceId || \"\" }\n });\n }\n } catch (e) {\n console.error(\"[GATEWAY] world tick / automations loop error:\", String(e?.message || e));\n }\n })();\n}, 50);\n\n(async () => {\n await rhizohRuntime.telemetry.initOpenTelemetry();\n if (String(process.env.CASTLE_GENESIS_DISK_PERSIST ?? \"\").trim() === \"\") {\n process.env.CASTLE_GENESIS_DISK_PERSIST = \"1\";\n }\n const genesisDiskBoot = await hydrateGenesisContinuityPersistenceBootV0();\n if (!genesisDiskBoot.ok) {\n console.warn(\"[GATEWAY] genesis disk hydrate failed; continuing with in-memory genesis continuity\");\n }\n httpServer.listen(PORT, () => {\n startGenesisCanonicalClock();\n installGenesisCheckpointSurfaceGetter(buildGenesisRuntimeSurfacePayloadLive);\n setGenesisContinuityAfterPublishHook((seq) => {\n noteGenesisCheckpointSeqCommitted(seq);\n noteGenesisContinuitySeqAcceptedV0(seq);\n });\n startGenesisContinuityInfraSampler(buildGenesisRuntimeSurfacePayloadLive, 2000);\n logProductionObservatorySurfaceGuardsV0();\n logGatewaySubstrateAuthorityGuardsV0();\n if (isLlmWorkerEnabledV0()) {\n try {\n const workerBoot = initLlmWorkerAtBootV0();\n console.log(\"[GATEWAY] llm worker boot:\", workerBoot);\n } catch (workerErr) {\n console.error(\"[GATEWAY] llm worker boot failed:\", workerErr);\n }\n }\n console.log(`[GATEWAY] ws/http://localhost:${PORT}`);\n console.log(\n `[GATEWAY] genesis continuity: GET ${rhizohRuntime.routes.genesisRuntime} | POST ${rhizohRuntime.routes.genesisIngress} | SSE ${rhizohRuntime.routes.genesisStream} | replay ${rhizohRuntime.routes.genesisReplay} | diff ${rhizohRuntime.routes.genesisReplayDiff} | equiv ${rhizohRuntime.routes.genesisReplayEquivalence} | analytics ${rhizohRuntime.routes.genesisReplayAnalytics} | evolution ${rhizohRuntime.routes.genesisReplayEvolution}`\n );\n if (String(process.env.CASTLE_GENESIS_OBSERVATORY_LIVE_V01 ?? \"\").trim() === \"1\") {\n console.log(\"GENESIS OBSERVATORY LIVE v0.1 — READ ONLY PHASE STARTED\");\n }\n });\n})().catch((e) => {\n console.error(\"[GATEWAY] fatal boot (before or during listen):\", e);\n process.exit(1);\n});\n"}, "5f86dff6a4ff1bf4a127cbf43cbae2aef1e4d85d": {"t": 1785754124.460345, "n": 176952, "h": "f799c0230632dcf0", "c": "import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport dotenv from \"dotenv\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconsole.log(\"🔥 [GENESIS_BOOT] entry file:\", __filename);\nconsole.log(\"🔥 [GENESIS_BOOT] node env:\", process.env.NODE_ENV);\nconsole.log(\"🔥 [GENESIS_BOOT] pid:\", process.pid);\nconsole.log(\"🧬 [GENESIS_BOOT] cwd:\", process.cwd());\nconsole.log(\"🧬 [GENESIS_BOOT] import.meta.url:\", import.meta.url);\ndotenv.config({ path: path.join(__dirname, \"..\", \".env\") });\ndotenv.config({ path: path.join(__dirname, \"..\", \".env.local\"), override: true });\n\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport { createServer } from \"node:http\";\nimport { createOrchestrator } from \"@castle/orchestrator\";\nimport { parseTextToCommand } from \"@castle/command-dsl\";\nimport {\n WS_MESSAGE,\n COMMAND,\n createEnvelope,\n createRhizohImmutableEventV0,\n createRhizohPayloadRefV0,\n safeJsonParse,\n validateRhizohVoiceLiveChunkPayloadV0\n} from \"@castle/protocol\";\nimport { queryOpenData } from \"./openData.js\";\nimport { verifyClientToken } from \"./auth.js\";\nimport { handleMatchMoveAuthorityV0 } from \"./rhizoh/matchMoveAuthorityV0.js\";\nimport { queryRhizohLlm } from \"./rhizohLlmGateway.js\";\nimport { rhizohGatewayTurn } from \"./rhizohGatewayTurn.js\";\nimport {\n enqueueLlmWorkerTaskAsyncV0,\n getLatestLlmWorkerTaskIdV0,\n getLlmWorkerDebugSnapshotV0,\n getLlmWorkerTaskSnapshotV0,\n initLlmWorkerAtBootV0,\n isLlmWorkerAsyncEnabledV0,\n isLlmWorkerEnabledV0,\n runLlmWorkerTaskV0\n} from \"./llmWorkerRuntime.js\";\nimport {\n RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n runRhizohVoiceTranscribeV3,\n rhizohVoiceTranscribeEnvV3\n} from \"./rhizohVoiceTranscribeV3.js\";\nimport { createHttpCorsPolicy, normalizeHttpOrigin, parseAllowedOriginsFromEnv } from \"./httpCorsPolicyV1.js\";\nimport {\n applyLanguagePropagationToLlmPayloadV1,\n applyLanguagePropagationToVoiceBodyV1,\n buildRhizohLanguagePropagationEchoV1,\n parseRhizohLanguagePropagationV1\n} from \"./rhizohLanguagePropagationV1.js\";\nimport { trimRhizohLlmGatewayPayloadV1 } from \"./rhizohLlmPayloadTrimV1.js\";\nimport {\n meshAppendDelta,\n meshContinuityAggregate,\n meshJoin,\n meshLeave,\n meshReplayFromNodeId,\n meshReplayFromSeq,\n meshSnapshot,\n meshSubscribeSse\n} from \"./presenceRoomMesh.js\";\nimport { runRhizohBrainV2 } from \"./rhizohBrainV2.js\";\nimport {\n listConnections,\n createConnection,\n updateConnection,\n deleteConnection,\n setDefaultConnection,\n resolveConnection\n} from \"./llmConnectionsStore.js\";\nimport { getCloudSyncSnapshotV0, mergeCloudSyncSnapshotV0 } from \"./castleCloudSyncStore.js\";\nimport {\n CASTLE_NETWORK_SIGNAL_V0,\n handleCastleNetworkSignalV0,\n listCastleNetworkPresenceV0,\n removeCastleNetworkClientV0,\n validateCastleNetworkSignalPayloadV0\n} from \"./castleNetworkRelayV0.js\";\nimport { checkHttpRateLimit, getHttpClientIp } from \"./castleHttpRateLimit.js\";\nimport {\n submitAbuseReportV0,\n listModerationQueueV0,\n softBlockUserV0,\n removeSoftBlockV0,\n listSoftBlockedUsersV0\n} from \"./ops/moderationMvpV0.js\";\nimport { listRecentAgentSnapshotsV0 } from \"./ops/agentObservabilityV0.js\";\nimport { readAgentContainmentConfigV0 } from \"./ops/agentContainmentV0.js\";\nimport { readCostContainmentConfigV0, getCostContainmentStatsV0 } from \"./ops/costContainmentV0.js\";\nimport { getGlobalCostLedgerSnapshotV0, getGclBackendStatusV0 } from \"./ops/globalCostLedgerV0.js\";\nimport { evaluateEconomicStrategyL3V0 } from \"./ops/economicStrategyEngineL3V0.js\";\nimport { getPhasedRolloutStatsV0, readPhasedRolloutTierV0 } from \"./ops/phasedRolloutV0.js\";\nimport { buildUnifiedStateNarrativeV0 } from \"./ops/unifiedStateNarrativeV0.js\";\nimport { resolveHardeningTenantIdV0 } from \"./ops/narrativeTenantIsolationV0.js\";\nimport {\n classifyStressResponseV0,\n verifyStressTaxonomyCoverageV0,\n verifyStressConflictResolutionV0\n} from \"./ops/stressResponseTaxonomyV0.js\";\nimport { verifyResolutionStabilityEnvelopeV0 } from \"./ops/resolutionStabilityEnvelopeV0.js\";\nimport { logLlmAccess } from \"./castleLlmAudit.js\";\nimport { appendMemory, listMemories, getMemoryContext, getPersonaGoalMemory, setPersonaGoalMemory, autoCompactMemories } from \"./memoryStore.js\";\nimport { getFirebasePersistence } from \"./firebasePersistence.js\";\nimport { registerAgentIdentity, listAgentIdentities, getAgentIdentity, updateAgentIdentity } from \"./agentIdentityStore.js\";\nimport { queueAcademyEvent, listAcademyEvents, resolveAcademyEvent, runAcademyEventTick } from \"./academyEventEngine.js\";\nimport { enqueuePdfJob, getPdfJob } from \"./pdfPipeline.js\";\nimport {\n createPublishSession,\n listPublishSessions,\n updatePublishSession,\n addTranscript,\n listTranscripts,\n searchMetadata,\n registerDevice,\n listDevices,\n updateDevice,\n appendTelemetry,\n listTelemetry,\n createAutomation,\n listAutomations,\n tickAutomations,\n enqueueCommand,\n listCommands,\n updateCommandStatus,\n registerSocialChannel,\n listSocialChannels\n} from \"./studioOpsStore.js\";\nimport { metrics as infraMetrics } from \"./infra/metrics.js\";\nimport { renderRhizohPrometheusMetrics, rhizohEnterpriseMetrics } from \"./infra/rhizohEnterpriseMetrics.js\";\nimport { scoreHealth } from \"./infra/healthScore.js\";\nimport { getTraceById, listTracesBySession } from \"./infra/traceRegistry.js\";\nimport { canonicalEpistemicSealString, hashAndSignEpistemicSeal, EPISTEMIC_SEAL_SCHEMA } from \"./epistemicSeal.js\";\nimport { persistEpistemicLedgerBatch } from \"./epistemicLedgerStore.js\";\nimport { persistEpistemicForecastBatch } from \"./epistemicForecastStore.js\";\nimport {\n persistAuthorityLedgerWitnessBatchV1,\n getAuthorityLedgerWitnessSnapshotV1,\n replayAuthorityLedgerWitnessChainV1\n} from \"./authorityLedgerWitnessStoreV1.js\";\nimport { assimilateAuthorityEpochMergeV1 } from \"./authorityEpochMergeStoreV1.js\";\nimport { buildRhizohExternalGroundTruthPayload } from \"./rhizohExternalGroundTruthGateway.js\";\nimport { ingestRhizohExternalLossBatchHttp } from \"./rhizohExternalLossIngestGateway.js\";\nimport {\n ingestRhizohProductOutcomeHttp,\n listRhizohProductOutcomeAggregates,\n verifyRhizohOutcomeHmac,\n verifyRhizohOutcomeSourceToken\n} from \"./rhizohProductOutcomeIngestGateway.js\";\nimport { initRhizoh } from \"./rhizohProductionBootstrap.js\";\nimport { handleAcademicObservatoryExportGetV0 } from \"./rhizoh/academicObservatoryHttpV0.js\";\nimport { handleCouncilAnomalyReasoningPostV0 } from \"./council/rhizohEpistemicCouncilHttpV0.js\";\nimport {\n handleLiveSportsBundleGetV0,\n handleLiveNewsHeadlinesGetV0,\n handleLiveWorldFeedGetV0\n} from \"./rhizoh/liveWorldFeedHttpV0.js\";\nimport {\n buildGenesisRuntimeSurfacePayload,\n recordGenesisEpistemicLedgerPersisted,\n recordGenesisEpistemicSealIssued,\n startGenesisCanonicalClock\n} from \"./genesisRuntimeSurfaceV0.js\";\nimport { registerGenesisSseClient, getGenesisContinuitySeq, setGenesisContinuityAfterPublishHook } from \"./genesisContinuityStreamHubV0.js\";\nimport { computeGenesisReplayFingerprintV0 } from \"./genesisReplayFingerprintV0.js\";\nimport { buildGenesisGatewayCapabilitiesV0 } from \"./genesisGatewayCapabilitiesV0.js\";\nimport {\n installGenesisCheckpointSurfaceGetter,\n noteGenesisCheckpointSeqCommitted,\n getLatestGenesisSignedCheckpoint\n} from \"./genesisContinuityCheckpointV0.js\";\nimport { hydrateGenesisContinuityPersistenceBootV0 } from \"./genesisContinuityHydrateBootV0.js\";\nimport { startGenesisContinuityInfraSampler } from \"./genesisContinuityInfraSamplerV0.js\";\nimport {\n genesisCheckpointQueryBySeqV0,\n genesisCheckpointQueryRangeV0,\n genesisCheckpointQueryLineageV0\n} from \"./genesisCheckpointQueryV0.js\";\nimport {\n genesisContinuityEventArchiveEnabled,\n queryGenesisContinuityEventArchiveV0,\n GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION\n} from \"./genesisContinuityEventArchiveV0.js\";\nimport { resolveGenesisReplayRouterV1, GENESIS_REPLAY_ROUTER_SCHEMA } from \"./genesisReplayRouterV1.js\";\nimport {\n computeGenesisReplayTemporalDiffV1,\n GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA\n} from \"./genesisReplayTemporalDiffV1.js\";\nimport { compareGenesisReplayEquivalenceV1 } from \"./genesisReplayEquivalenceV1.js\";\nimport { computeGenesisReplayAnalyticsV1 } from \"./genesisReplayAnalyticsV1.js\";\nimport { computeGenesisReplayEvolutionV1 } from \"./genesisReplayEvolutionV1.js\";\nimport { handleStripeCheckoutCreate, handleStripeMembershipWebhook } from \"./stripeWebhookMembership.js\";\nimport { buildRealityHealthPayload } from \"./edgeRealityConsistencyV1.js\";\nimport {\n assertWalPeerSocketAuthorityV0,\n buildGatewaySubstrateAuthoritySnapshotV0,\n logGatewaySubstrateAuthorityGuardsV0,\n validateWalPeerFeedSignatureV0\n} from \"./gatewaySubstrateAuthorityV0.js\";\nimport {\n buildSubstrateOperationalSnapshotV0,\n recordWalPeerRejectV0,\n renderSubstratePrometheusMetricsV0\n} from \"./infra/substrateOperationalMetrics.js\";\nimport { ingestClientSubstrateHealthV0 } from \"./rhizohSubstrateHealthIngestV0.js\";\nimport { recordSubstrateOtelEventV0 } from \"./infra/opentelemetrySubstrateV0.js\";\nimport {\n GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n getGenesisWorldObservationIngressStatusV0,\n ingestGenesisWorldObservationV0\n} from \"./genesisContinuityClientIngressV0.js\";\nimport {\n getGenesisContinuitySeqAuditV0,\n noteGenesisContinuitySeqAcceptedV0\n} from \"./genesisContinuitySeqGapV0.js\";\n\n/** Render / Fly / Railway: `PORT` — yoksa `CASTLE_GATEWAY_PORT` — yoksa 8090. */\nconst PORT =\n Number(process.env.PORT) ||\n Number(process.env.CASTLE_GATEWAY_PORT) ||\n 8090;\n/** Spiral / world WebSocket server — set after `wss` init; used by genesis runtime surface. */\nlet spiralWssForGenesis = null;\nconst MAX_MESSAGE_BYTES = Number(process.env.CASTLE_MAX_MESSAGE_BYTES || 32 * 1024);\nconst REQUIRED_GATEWAY_TOKEN = process.env.CASTLE_GATEWAY_TOKEN || \"\";\nconst ALLOWED_ORIGINS = parseAllowedOriginsFromEnv().fromList;\nconst httpCors = createHttpCorsPolicy();\n\n/** @returns {string|null} `Access-Control-Allow-Origin`; null = browser CORS fail. */\nfunction accessControlAllowOriginValue(req) {\n return httpCors.accessControlAllowOriginValue(req);\n}\n\nfunction applyHttpCorsHeaders(req, res) {\n return httpCors.applyHttpCorsHeaders(req, res);\n}\nconst REQUIRE_AUTH = process.env.CASTLE_REQUIRE_AUTH === \"true\";\nconst ALLOW_DEV_ANON = process.env.CASTLE_ALLOW_DEV_ANON !== \"false\";\nconst ALLOW_DEV_HTTP_UID = process.env.CASTLE_ALLOW_DEV_HTTP_UID !== \"false\";\n/** Presence mesh: allow `X-Castle-Guest-Id` when Firebase auth absent (default on). Set `false` to require auth only. */\nconst ALLOW_MESH_GUEST = process.env.CASTLE_ALLOW_MESH_GUEST !== \"false\";\nconst SOCIAL_RETRY_MAX = Math.max(0, Math.min(5, Number(process.env.CASTLE_SOCIAL_RETRY_MAX || 2)));\nconst SOCIAL_RETRY_BASE_MS = Math.max(100, Number(process.env.CASTLE_SOCIAL_RETRY_BASE_MS || 700));\nconst TELEGRAM_BOT_TOKEN = process.env.CASTLE_TELEGRAM_BOT_TOKEN || \"\";\nconst WHATSAPP_TOKEN = process.env.CASTLE_WHATSAPP_TOKEN || \"\";\nconst WHATSAPP_PHONE_NUMBER_ID = process.env.CASTLE_WHATSAPP_PHONE_NUMBER_ID || \"\";\nconst RL_RHIZOH_LLM_PER_MIN = Math.max(5, Number(process.env.CASTLE_RL_RHIZOH_LLM_PER_MIN || 40));\nconst RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN || 30)\n);\nconst RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN || 120));\nconst RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN = Math.max(\n 10,\n Number(process.env.CASTLE_RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN || 60)\n);\nconst RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN = Math.max(8, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN || 48));\nconst RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN = Math.max(4, Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN || 24));\nconst RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN || 18)\n);\nconst RL_EPISTEMIC_SEAL_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_EPISTEMIC_SEAL_PER_MIN || 120));\nconst RL_EPISTEMIC_LOG_PER_MIN = Math.max(20, Number(process.env.CASTLE_RL_EPISTEMIC_LOG_PER_MIN || 240));\nconst EPISTEMIC_SEAL_SECRET = String(\n process.env.CASTLE_EPISTEMIC_SEAL_SECRET || process.env.CASTLE_GATEWAY_TOKEN || \"\"\n).trim();\nconst RL_LLM_CONN_TEST_PER_MIN = Math.max(2, Number(process.env.CASTLE_RL_LLM_CONNECTION_TEST_PER_MIN || 8));\n\n/** One-shot production warnings: CORS surface, auth toggle, dev bypass flags (observatory deploy hygiene). */\nfunction logProductionObservatorySurfaceGuardsV0() {\n if (String(process.env.NODE_ENV || \"\").toLowerCase() !== \"production\") return;\n const tag = \"[GATEWAY_PROD_GUARD]\";\n const primaryCors = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n const corsListEmpty = ALLOWED_ORIGINS.length === 0;\n if (corsListEmpty && !primaryCors) {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty and CASTLE_HTTP_CORS_ORIGIN empty — production still merges RHIZOH_BROWSER_ORIGINS_V1 defaults; set env for preview/staging hosts`\n );\n } else if (corsListEmpty && primaryCors && primaryCors !== \"*\") {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty — only CASTLE_HTTP_CORS_ORIGIN set; add web.app + firebaseapp.com (and previews) to CASTLE_ALLOWED_ORIGINS if clients use alternate hostnames`\n );\n }\n if (primaryCors && /localhost|127\\.0\\.0\\.1/i.test(primaryCors)) {\n console.warn(`${tag} CORS: CASTLE_HTTP_CORS_ORIGIN is localhost while NODE_ENV=production`);\n }\n if (REQUIRE_AUTH) {\n console.warn(\n `${tag} AUTH: CASTLE_REQUIRE_AUTH=true — clients must send a valid gateway credential (otherwise silent 401 on replay/SSE); EventSource cannot set custom headers unless proxied`\n );\n if (REQUIRED_GATEWAY_TOKEN.length < 16) {\n console.warn(`${tag} AUTH: CASTLE_GATEWAY_TOKEN missing or short while require-auth enabled`);\n }\n }\n if (ALLOW_DEV_ANON) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_ANON is not \"false\" — dev anonymous path may be live in production (set CASTLE_ALLOW_DEV_ANON=false)`\n );\n }\n if (ALLOW_DEV_HTTP_UID) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_HTTP_UID is not \"false\" — X-Castle-Dev-Uid bypass risk (set CASTLE_ALLOW_DEV_HTTP_UID=false)`\n );\n }\n}\n\nconst rhizohRuntime = initRhizoh();\n\n/** İstemci: llmKeySource veya keySource — env | user_connection | auto */\nfunction normalizeClientLlmKeySource(raw) {\n const s = String(raw ?? \"\").trim().toLowerCase();\n if (s === \"env\" || s === \"server\") return \"env\";\n if (s === \"user_connection\" || s === \"connection\" || s === \"user\") return \"user_connection\";\n if (s === \"auto\" || s === \"\") return \"auto\";\n return null;\n}\n\n/**\n * Server-side provider wins for env/auto unless user_connection — client bundle may still send provider=openai.\n * @param {\"env\"|\"user_connection\"|\"auto\"} keyMode\n * @param {{ provider?: string, model?: string, apiKey?: string } | null} conn\n * @param {{ provider?: string, model?: string }} payload\n */\nfunction resolveServerLlmProviderV0(keyMode, conn, payload) {\n const envProvider = String(process.env.CASTLE_LLM_PROVIDER || \"\").trim() || undefined;\n const envModel = String(process.env.CASTLE_LLM_MODEL || \"\").trim() || undefined;\n\n if (keyMode === \"user_connection\") {\n return {\n resolvedProvider: conn?.provider || payload?.provider,\n resolvedModel: conn?.model || payload?.model,\n connApiKey: conn?.apiKey || \"\"\n };\n }\n\n if (keyMode === \"env\") {\n return {\n resolvedProvider: envProvider ?? payload?.provider,\n resolvedModel: envModel ?? payload?.model,\n connApiKey: \"\"\n };\n }\n\n if (conn?.provider && conn?.apiKey) {\n return {\n resolvedProvider: conn.provider,\n resolvedModel: conn.model,\n connApiKey: conn.apiKey\n };\n }\n\n return {\n resolvedProvider: envProvider ?? payload?.provider,\n resolvedModel: envModel ?? payload?.model,\n connApiKey: \"\"\n };\n}\n\nfunction jsonReplacerSafeV0(_key, value) {\n if (typeof value === \"bigint\") return Number(value);\n return value;\n}\n\nfunction sendJson(res, statusCode, payload) {\n res.writeHead(statusCode, { \"Content-Type\": \"application/json; charset=utf-8\" });\n try {\n res.end(JSON.stringify(payload, jsonReplacerSafeV0));\n } catch (error) {\n res.end(\n JSON.stringify({\n ok: false,\n error: \"response_serialize_failed\",\n detail: String(error?.message || error)\n })\n );\n }\n}\n\nfunction logRhizohHealth(stage, detail = {}) {\n try {\n console.info(`[CASTLE_${String(stage || \"unknown\")}]`, detail && typeof detail === \"object\" ? detail : {});\n } catch {\n /* noop */\n }\n}\n\nfunction renderInfraMetricsProm() {\n const lines = [\n `castle_gateway_events_processed_total ${infraMetrics.eventsProcessed}`,\n `castle_gateway_duplicate_reject_total ${infraMetrics.duplicateRejects}`,\n `castle_gateway_enqueue_latency_ms ${infraMetrics.enqueueLatencyMs}`,\n `castle_gateway_queue_depth ${infraMetrics.queueDepth}`,\n `castle_gateway_queue_lag_seconds ${(Number(infraMetrics.queueLag || 0) / 1000).toFixed(3)}`,\n `castle_gateway_errors_total ${infraMetrics.errors}`\n ];\n const buckets = infraMetrics.enqueueLatencyBuckets || {};\n for (const k of Object.keys(buckets)) {\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"${k}\"} ${buckets[k]}`);\n }\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"+Inf\"} ${infraMetrics.eventsProcessed}`);\n if (process.env.CASTLE_SUBSTRATE_METRICS !== \"0\") {\n lines.push(renderSubstratePrometheusMetricsV0().trimEnd());\n }\n return lines.join(\"\\n\");\n}\n\nasync function fetchJsonWithTimeout(url, timeoutMs = 1200) {\n const ctl = new AbortController();\n const tid = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const res = await fetch(url, { signal: ctl.signal });\n if (!res.ok) return null;\n return await res.json();\n } catch {\n return null;\n } finally {\n clearTimeout(tid);\n }\n}\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction getHttpPathname(req) {\n let p = String(req.url || \"\").split(\"?\")[0];\n /* Reverse proxies / clients sometimes append `/` — avoid silent 404 on e.g. `/rhizoh/genesis/stream/`. */\n while (p.length > 1 && p.endsWith(\"/\")) {\n p = p.slice(0, -1);\n }\n return p;\n}\n\nfunction rhizohLlmEnvConfigured() {\n const p = String(process.env.CASTLE_LLM_PROVIDER || \"openai\").toLowerCase();\n const key =\n p === \"anthropic\"\n ? process.env.ANTHROPIC_API_KEY\n : p === \"gemini\"\n ? process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY\n : p === \"xai\"\n ? process.env.XAI_API_KEY\n : p === \"deepseek\"\n ? process.env.DEEPSEEK_API_KEY\n : p === \"mistral\"\n ? process.env.MISTRAL_API_KEY\n : p === \"openrouter\"\n ? process.env.OPENROUTER_API_KEY\n : process.env.OPENAI_API_KEY;\n return !!String(key || \"\").trim();\n}\n\n/**\n * @param {import(\"firebase-admin/firestore\").Firestore | null} db\n * @param {number} timeoutMs\n */\nasync function probeFirestoreReachable(db, timeoutMs = 2000) {\n if (!db) return { ok: false, reason: \"no_db\" };\n const timeout = sleep(timeoutMs).then(() => ({ ok: false, reason: \"timeout\" }));\n const work = db\n .collection(\"_castle_gateway_health\")\n .doc(\"ping\")\n .get()\n .then(() => ({ ok: true, reason: \"reachable\" }))\n .catch((e) => ({ ok: false, reason: String(e?.message || e || \"firestore_error\") }));\n return Promise.race([work, timeout]);\n}\n\nfunction stripHtml(text = \"\") {\n return String(text)\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \" \")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \" \")\n .replace(/<[^>]+>/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction haversineMeters(aLat, aLon, bLat, bLon) {\n const R = 6371000;\n const dLat = ((bLat - aLat) * Math.PI) / 180;\n const dLon = ((bLon - aLon) * Math.PI) / 180;\n const sa = Math.sin(dLat / 2);\n const sb = Math.sin(dLon / 2);\n const x = sa * sa + Math.cos((aLat * Math.PI) / 180) * Math.cos((bLat * Math.PI) / 180) * sb * sb;\n return 2 * R * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));\n}\n\nasync function checkPublishTargetHealth(protocol, target) {\n const p = String(protocol || \"\").toUpperCase();\n const t = String(target || \"\").trim();\n if (!t) return { ok: false, reason: \"target_missing\", shouldReconnect: false };\n if (p === \"RTMP\") {\n const ok = t.startsWith(\"rtmp://\") || t.startsWith(\"rtmps://\");\n return { ok, reason: ok ? \"rtmp_target_format_ok\" : \"invalid_rtmp_target\", shouldReconnect: !ok, reconnectInSec: 15 };\n }\n if (p === \"WHIP\" || p === \"SFU\") {\n try {\n const res = await fetch(t, { method: \"OPTIONS\" });\n const ok = res.status < 500;\n return { ok, reason: `http_${res.status}`, shouldReconnect: !ok, reconnectInSec: 8 };\n } catch {\n return { ok: false, reason: \"network_unreachable\", shouldReconnect: true, reconnectInSec: 8 };\n }\n }\n return { ok: true, reason: \"unchecked_protocol\", shouldReconnect: false };\n}\n\nasync function fetchTextFromUrl(url) {\n const res = await fetch(url, { method: \"GET\" });\n if (!res.ok) throw new Error(`fetch_${res.status}`);\n const ct = String(res.headers.get(\"content-type\") || \"\");\n if (ct.includes(\"application/pdf\")) throw new Error(\"pdf_binary_not_supported_directly\");\n const body = await res.text();\n return stripHtml(body).slice(0, 14000);\n}\n\nfunction buildTaskActionGraph(task = \"\", context = {}) {\n const t = String(task || \"\").toLowerCase();\n if (t.includes(\"dinner\") || t.includes(\"akşam yemeği\") || t.includes(\"aksam yemegi\")) {\n return {\n taskTitle: \"Dinner Prep Planner\",\n nodes: [\n { id: \"n1\", title: \"Kitchen preheat\", action: \"smart-oven.preheat\", params: { temperatureC: 190, mode: \"fan\" } },\n { id: \"n2\", title: \"Lighting comfort\", action: \"smart-light.set_scene\", params: { scene: \"warm_dinner\" } },\n { id: \"n3\", title: \"Fridge inventory check\", action: \"smart-fridge.inventory_scan\", params: {} },\n { id: \"n4\", title: \"Reminder notification\", action: \"smart-watch.notify\", params: { text: \"Dinner prep started.\" } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" },\n { from: \"n3\", to: \"n4\" }\n ]\n };\n }\n return {\n taskTitle: task || \"General Planner\",\n nodes: [\n { id: \"n1\", title: \"Safety check\", action: \"device.safety_check\", params: { context } },\n { id: \"n2\", title: \"Primary action\", action: \"device.execute_task\", params: { task } },\n { id: \"n3\", title: \"Report\", action: \"studio.timeline_note\", params: { task } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" }\n ]\n };\n}\n\nfunction buildEthicsPrograms() {\n return {\n policyVersion: \"v1\",\n tracks: [\n {\n id: \"elderly-care-assist\",\n audience: \"elderly\",\n title: \"Elderly Assistance Track\",\n principles: [\"human-in-the-loop\", \"consent-first\", \"low-friction alerts\"],\n modules: [\"medication-reminder\", \"fall-risk-check\", \"family-notify\", \"daily-companion-brief\"]\n },\n {\n id: \"kids-safe-learning\",\n audience: \"children\",\n title: \"Kids Safe Learning Track\",\n principles: [\"guardian-consent\", \"age-appropriate-language\", \"content-filtering\"],\n modules: [\"study-coach\", \"screen-time-balance\", \"safe-social-share\", \"emergency-contact\"]\n }\n ]\n };\n}\n\nasync function deliverViaTelegram(channel, text) {\n const chatId = String(channel.endpoint || \"\").trim();\n if (!TELEGRAM_BOT_TOKEN) throw new Error(\"telegram_token_missing\");\n if (!chatId) throw new Error(\"telegram_chat_id_missing\");\n const url = `https://api.telegram.org/bot${encodeURIComponent(TELEGRAM_BOT_TOKEN)}/sendMessage`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ chat_id: chatId, text })\n });\n if (!res.ok) throw new Error(`telegram_http_${res.status}`);\n return { provider: \"telegram\", status: res.status };\n}\n\nasync function deliverViaWhatsapp(channel, text) {\n const to = String(channel.endpoint || \"\").trim();\n if (!WHATSAPP_TOKEN) throw new Error(\"whatsapp_token_missing\");\n if (!WHATSAPP_PHONE_NUMBER_ID) throw new Error(\"whatsapp_phone_number_id_missing\");\n if (!to) throw new Error(\"whatsapp_to_missing\");\n const url = `https://graph.facebook.com/v22.0/${encodeURIComponent(WHATSAPP_PHONE_NUMBER_ID)}/messages`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${WHATSAPP_TOKEN}` },\n body: JSON.stringify({ messaging_product: \"whatsapp\", to, type: \"text\", text: { body: text } })\n });\n if (!res.ok) throw new Error(`whatsapp_http_${res.status}`);\n return { provider: \"whatsapp\", status: res.status };\n}\n\nasync function deliverViaWebhook(channel, text) {\n const endpoint = String(channel.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"webhook_endpoint_missing\");\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text, platform: channel.platform || \"webhook\", ts: Date.now() })\n });\n if (!res.ok) throw new Error(`webhook_http_${res.status}`);\n return { provider: \"webhook\", status: res.status };\n}\n\nasync function deliverSocialMessage(channel, text) {\n const platform = String(channel.platform || \"webhook\").toLowerCase();\n if (platform === \"telegram\") return deliverViaTelegram(channel, text);\n if (platform === \"whatsapp\") return deliverViaWhatsapp(channel, text);\n return deliverViaWebhook(channel, text);\n}\n\nasync function deliverSocialWithRetry(channel, text) {\n let attempt = 0;\n let lastError = null;\n while (attempt <= SOCIAL_RETRY_MAX) {\n try {\n const result = await deliverSocialMessage(channel, text);\n return { ok: true, attempt: attempt + 1, ...result };\n } catch (error) {\n lastError = error;\n if (attempt >= SOCIAL_RETRY_MAX) break;\n const jitter = Math.floor(Math.random() * 90);\n await sleep(SOCIAL_RETRY_BASE_MS * Math.pow(2, attempt) + jitter);\n }\n attempt += 1;\n }\n return { ok: false, attempt: attempt + 1, error: String(lastError?.message || \"delivery_failed\") };\n}\n\nasync function dispatchCommandToAdapter(device, command) {\n const adapter = String(command?.adapter || device?.adapter || \"websocket\").toLowerCase();\n const endpoint = String(device?.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"device_endpoint_missing\");\n if (adapter === \"mqtt\") {\n // MQTT delivery is proxied via a bridge endpoint (HTTP webhook style).\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n type: \"mqtt-command\",\n topic: `castle/devices/${device.id}/commands`,\n action: command.action,\n params: command.params || {}\n })\n });\n if (!res.ok) throw new Error(`mqtt_bridge_http_${res.status}`);\n return { ok: true, adapter, via: \"http-bridge\", status: res.status };\n }\n if (!endpoint.startsWith(\"ws://\") && !endpoint.startsWith(\"wss://\")) throw new Error(\"invalid_websocket_endpoint\");\n const sendResult = await new Promise((resolve, reject) => {\n const ws = new WebSocket(endpoint);\n const timer = setTimeout(() => {\n try {\n ws.close();\n } catch {\n /* noop */\n }\n reject(new Error(\"websocket_timeout\"));\n }, 3000);\n ws.on(\"open\", () => {\n ws.send(\n JSON.stringify({\n type: \"castle-command\",\n commandId: command.id,\n deviceId: command.deviceId,\n action: command.action,\n params: command.params || {}\n }),\n (err) => {\n clearTimeout(timer);\n ws.close();\n if (err) reject(new Error(\"websocket_send_failed\"));\n else resolve({ ok: true, adapter: \"websocket\", via: \"ws\" });\n }\n );\n });\n ws.on(\"error\", () => {\n clearTimeout(timer);\n reject(new Error(\"websocket_connect_failed\"));\n });\n });\n return sendResult;\n}\n\nfunction readHttpJson(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => {\n if (!buf) return resolve({});\n const payload = safeJsonParse(buf);\n if (!payload || typeof payload !== \"object\") return reject(new Error(\"invalid_json_payload\"));\n resolve(payload);\n });\n req.on(\"error\", reject);\n });\n}\n\n/** Ham gövde (HMAC doğrulaması için). */\nfunction readHttpBodyText(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => resolve(buf));\n req.on(\"error\", reject);\n });\n}\n\nfunction maskOpaqueId(value) {\n const t = String(value || \"\");\n if (!t) return \"(empty)\";\n if (t.length <= 8) return \"****\";\n return `${t.slice(0, 4)}…${t.slice(-2)}`;\n}\n\nfunction readGatewayHttpToken(req) {\n const x = String(req.headers?.[\"x-castle-gateway-token\"] || \"\").trim();\n if (x) return x;\n const auth = String(req.headers?.authorization || \"\");\n const m = /^Bearer\\s+(.+)$/i.exec(auth);\n return m ? String(m[1] || \"\").trim() : \"\";\n}\n\nasync function resolveHttpUser(req) {\n const authResult = await verifyClientToken(req);\n if (authResult?.ok && authResult.user?.uid) {\n return { ok: true, uid: String(authResult.user.uid), mode: authResult.kind || \"auth\" };\n }\n if (ALLOW_DEV_ANON && ALLOW_DEV_HTTP_UID) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) return { ok: true, uid: `dev-${devUid.slice(0, 64)}`, mode: \"dev-header\" };\n }\n return { ok: false, reason: authResult?.reason || \"auth_required\" };\n}\n\n/**\n * Epistemic ledger ingest — Firebase uid OR valid transport token + X-Castle-Dev-Uid.\n * Prod disables CASTLE_ALLOW_DEV_HTTP_UID; LLM may still work while telemetry 401'd without this.\n */\nasync function resolveEpistemicIngestActor(req) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok && tok === REQUIRED_GATEWAY_TOKEN) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) {\n return { ok: true, uid: `gw-telemetry-${devUid.slice(0, 64)}`, mode: \"gateway-token-dev-uid\" };\n }\n return { ok: false, reason: \"dev_uid_required\" };\n }\n if (tok) {\n return { ok: false, reason: \"gateway_token_mismatch\" };\n }\n }\n const auth = await resolveHttpUser(req);\n if (auth.ok) return auth;\n return { ok: false, reason: auth.reason || \"auth_required\" };\n}\n\n/** Mesh actor: Firebase uid, dev header, or `guest:<uuid>` via header / `guestId` query (SSE cannot set headers). */\nasync function resolveMeshActor(req) {\n const auth = await resolveHttpUser(req);\n if (auth.ok) return { ok: true, clientUid: auth.uid, mode: auth.mode };\n if (ALLOW_MESH_GUEST) {\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const gid = String(req.headers?.[\"x-castle-guest-id\"] || q.searchParams.get(\"guestId\") || \"\").trim();\n if (gid) {\n const normalized = gid.startsWith(\"guest:\") ? gid.slice(0, 180) : `guest:${gid.slice(0, 160)}`;\n return { ok: true, clientUid: normalized, mode: \"guest-header\" };\n }\n }\n return { ok: false, reason: \"mesh_auth_required\" };\n}\n\n/** Live genesis runtime JSON (GET /rhizoh/genesis/runtime + SSE infra sampler). */\nasync function buildGenesisRuntimeSurfacePayloadLive() {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n let spiralWsActive = 0;\n if (spiralWssForGenesis) {\n for (const c of spiralWssForGenesis.clients) {\n if (c.readyState === 1) spiralWsActive += 1;\n }\n }\n const base = buildGenesisRuntimeSurfacePayload({\n infraMetrics,\n scoredHealth: scored,\n rhizohEnterpriseMetrics,\n mesh: meshContinuityAggregate(),\n workerHealth: worker,\n port: PORT,\n spiralWebSocketClientsActive: spiralWsActive\n });\n return {\n ...base,\n replayFingerprint: computeGenesisReplayFingerprintV0(base),\n gatewayCapabilities: buildGenesisGatewayCapabilitiesV0(),\n genesisStream: {\n schema: \"castle.genesis.stream_cursor.v0\",\n lastAcceptedSeq: getGenesisContinuitySeq(),\n clientIngress: {\n schema: GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n contract: \"castle.world_observation.ingress_envelope.v1\",\n postPath: rhizohRuntime.routes.genesisIngress,\n closure: getGenesisWorldObservationIngressStatusV0()\n },\n seqContinuity: getGenesisContinuitySeqAuditV0(),\n eventArchive: genesisContinuityEventArchiveEnabled()\n ? {\n enabled: true,\n projection: GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION,\n queryPath: rhizohRuntime.routes.genesisContinuityEvents\n }\n : { enabled: false },\n replayRouter: {\n schema: GENESIS_REPLAY_ROUTER_SCHEMA,\n queryPath: rhizohRuntime.routes.genesisReplay,\n queryHint: \"from=&to= or range=from-to; optional type=&checkpoints=0\",\n temporalDiffPath: rhizohRuntime.routes.genesisReplayDiff,\n temporalDiffSchema: GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA,\n equivalencePath: rhizohRuntime.routes.genesisReplayEquivalence,\n analyticsPath: rhizohRuntime.routes.genesisReplayAnalytics,\n evolutionPath: rhizohRuntime.routes.genesisReplayEvolution\n }\n }\n };\n}\n\nconst httpServer = createServer(async (req, res) => {\n const corsAllow = applyHttpCorsHeaders(req, res);\n if (req.method === \"OPTIONS\") {\n if (!corsAllow && req.headers?.origin) {\n console.warn(\"[GATEWAY_CORS] preflight rejected for origin:\", req.headers.origin);\n }\n res.writeHead(corsAllow ? 204 : 403);\n res.end();\n return;\n }\n\n const pathname = getHttpPathname(req);\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/genesis/__ping\") {\n sendJson(res, 200, {\n ok: true,\n pid: process.pid,\n entry: __filename,\n time: Date.now(),\n genesis: true\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/epistemic/__probe\") {\n sendJson(res, 200, {\n ok: true,\n epistemicIngestActorV2: true,\n routes: {\n seal: rhizohRuntime.routes.epistemicSeal,\n logsBatch: rhizohRuntime.routes.epistemicLogsBatch,\n authorityLedgerBatch: rhizohRuntime.routes.authorityLedgerBatch,\n authorityLedgerReplay: rhizohRuntime.routes.authorityLedgerReplay,\n authorityEpochMerge: rhizohRuntime.routes.authorityEpochMerge\n },\n hasGatewayToken: Boolean(REQUIRED_GATEWAY_TOKEN),\n gatewayTokenLen: REQUIRED_GATEWAY_TOKEN ? REQUIRED_GATEWAY_TOKEN.length : 0\n });\n return;\n }\n\n if (\n req.method === \"GET\" &&\n (pathname === \"/rhizoh/live/world-feed\" ||\n pathname === \"/rhizoh/live/sports-bundle\" ||\n pathname === \"/rhizoh/live/news-headlines\")\n ) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_live_world_feed:${ip}`, RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const out =\n pathname === \"/rhizoh/live/sports-bundle\"\n ? await handleLiveSportsBundleGetV0()\n : pathname === \"/rhizoh/live/news-headlines\"\n ? await handleLiveNewsHeadlinesGetV0(req)\n : await handleLiveWorldFeedGetV0(req);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/webhooks/stripe\") {\n await handleStripeMembershipWebhook(req, res);\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/billing/stripe/checkout\") {\n await handleStripeCheckoutCreate(req, res, { readHttpJson, resolveHttpUser, sendJson });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/live\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, live: false, dns: true, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n sendJson(res, 200, { ok: true, live: true, dns: true, service: \"castle-gateway\", ts: Date.now() });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/cors\") {\n const reqOrigin = normalizeHttpOrigin(req.headers?.origin);\n const allowOrigin = accessControlAllowOriginValue(req);\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n requestOrigin: reqOrigin || null,\n allowed: Boolean(allowOrigin),\n allowOrigin: allowOrigin || null,\n allowSetSize: httpCors.allowSet.size\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/ready\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, ready: false, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n try {\n const persistence = getFirebasePersistence();\n const llmProbe = rhizohLlmEnvConfigured();\n sendJson(res, 200, {\n ok: true,\n ready: true,\n dns: true,\n persistence: persistence.mode,\n service: \"castle-gateway\",\n gateway: \"ok\",\n llm: llmProbe ? \"ok\" : \"degraded\",\n spine: llmProbe ? \"ok\" : \"partial\"\n });\n } catch (e) {\n sendJson(res, 503, { ok: false, ready: false, reason: String(e?.message || e), service: \"castle-gateway\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/reality\") {\n const payload = buildRealityHealthPayload();\n sendJson(res, payload.ok ? 200 : 503, payload);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/substrate\") {\n const authority = buildGatewaySubstrateAuthoritySnapshotV0();\n const operational = buildSubstrateOperationalSnapshotV0();\n const drift = buildRealityHealthPayload();\n const ok = Boolean(authority.productionAuthoritySatisfied) && drift.ok;\n sendJson(res, ok ? 200 : 503, {\n ok,\n service: \"castle-gateway\",\n ts: Date.now(),\n authority,\n operational,\n drift: { ok: drift.ok, errors: drift.drift?.errors, warnings: drift.drift?.warnings }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.councilAnomalyReasoning) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_council_anomaly:${ip}`, 30, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const body = await readHttpJson(req);\n const out = handleCouncilAnomalyReasoningPostV0(body);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/substrate/health\") {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const body = await readHttpJson(req);\n const uid = (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestClientSubstrateHealthV0(body, { uid, clientId: uid });\n if (!result.ok) {\n sendJson(res, 400, result);\n return;\n }\n recordSubstrateOtelEventV0(\"client_health_ingest\", { uid: String(uid).slice(0, 32) });\n sendJson(res, 200, result);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/deps\") {\n const t0 = Date.now();\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, {\n ok: false,\n dns: true,\n llm: false,\n firestore: false,\n phase: \"maintenance\",\n latencyMs: Date.now() - t0,\n service: \"castle-gateway\"\n });\n return;\n }\n const persistence = getFirebasePersistence();\n let firestoreOk = persistence.mode !== \"firebase\";\n let firestoreDetail = persistence.mode === \"file\" ? \"file_mode\" : \"not_applicable\";\n if (persistence.mode === \"firebase\" && persistence.db) {\n const pr = await probeFirestoreReachable(persistence.db, 2000);\n firestoreOk = pr.ok;\n firestoreDetail = pr.reason || (pr.ok ? \"reachable\" : \"fail\");\n } else if (persistence.mode === \"firebase\" && !persistence.db) {\n firestoreOk = false;\n firestoreDetail = \"firebase_not_initialized\";\n }\n const llmOk = rhizohLlmEnvConfigured();\n const overallOk = llmOk && firestoreOk;\n const latencyMs = Date.now() - t0;\n sendJson(res, 200, {\n ok: overallOk,\n dns: true,\n llm: llmOk,\n firestore: persistence.mode === \"firebase\" ? firestoreOk : false,\n persistence: persistence.mode,\n firestoreDetail,\n latencyMs,\n service: \"castle-gateway\",\n wsPort: PORT\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health\") {\n const persistence = getFirebasePersistence();\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n wsPort: PORT,\n persistence: persistence.mode,\n presenceMesh: \"sse\"\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/health\") {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n sendJson(res, 200, {\n ok: true,\n role: \"gateway\",\n queueLagMs: infraMetrics.queueLag,\n queueDepth: infraMetrics.queueDepth,\n errors: infraMetrics.errors,\n status: scored.status,\n score: scored.score,\n reasons: scored.reasons,\n worker: worker\n ? {\n status: worker.status,\n score: worker.score,\n reasons: worker.reasons\n }\n : null,\n timestamp: Date.now()\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisRuntime) {\n const payload = await buildGenesisRuntimeSurfacePayloadLive();\n sendJson(res, 200, payload);\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.genesisIngress) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const actor = await resolveMeshActor(req);\n const clientId = actor.ok ? actor.clientUid : (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestGenesisWorldObservationV0(body, { clientId });\n if (!result.ok) {\n const status =\n result.error === \"rate_limited\" || result.error === \"agent_spoke_throttled\"\n ? 429\n : result.error === \"payload_too_large\"\n ? 413\n : 400;\n sendJson(res, status, result);\n return;\n }\n sendJson(res, result.idempotent ? 200 : 202, result);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisStream) {\n const corsOrigin = accessControlAllowOriginValue(req);\n if (!corsOrigin && String(process.env.NODE_ENV || \"\").toLowerCase() === \"production\") {\n const og = String(req.headers?.origin || \"\").trim();\n if (og) {\n console.warn(\"[GATEWAY] genesis SSE: missing Access-Control-Allow-Origin for browser Origin (CORS whitelist mismatch):\", og);\n }\n }\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n res.write(\"retry: 3000\\n\\n\");\n res.write(\": genesis continuity stream\\n\\n\");\n res.write(\"event: genesis\\n\");\n res.write(`data: {\"booted\":true,\"pid\":${process.pid}}\\n\\n`);\n registerGenesisSseClient(res, req);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLatest) {\n const cp = getLatestGenesisSignedCheckpoint();\n if (!cp) {\n sendJson(res, 404, { ok: false, error: \"no_checkpoint_yet\", hint: \"wait_for_seq_interval_or_configure_signing_secret\" });\n return;\n }\n sendJson(res, 200, { ok: true, checkpoint: cp });\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(rhizohRuntime.routes.genesisCheckpointBySeqPrefix)) {\n const tail = pathname.slice(rhizohRuntime.routes.genesisCheckpointBySeqPrefix.length).split(\"/\")[0];\n const seq = parseInt(String(tail || \"\").trim(), 10);\n const out = await genesisCheckpointQueryBySeqV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"checkpoint_not_found\") {\n sendJson(res, 404, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointRange) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryRangeV0(from, to);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"range_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLineage) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const seq = parseInt(String(u.searchParams.get(\"seq\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryLineageV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"lineage_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisContinuityEvents) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const limit = parseInt(String(u.searchParams.get(\"limit\") || \"\").trim(), 10);\n const out = await queryGenesisContinuityEventArchiveV0(from, to, type, limit);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\" || out.error === \"event_archive_disabled\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"archive_file_too_large\") {\n sendJson(res, 413, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplay) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await resolveGenesisReplayRouterV1({\n from: fromSeq,\n to: toSeq,\n type,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayDiff) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const out = await computeGenesisReplayTemporalDiffV1({ from: fromSeq, to: toSeq, type });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEquivalence) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from1 = parseInt(String(u.searchParams.get(\"from1\") || \"\").trim(), 10);\n const to1 = parseInt(String(u.searchParams.get(\"to1\") || \"\").trim(), 10);\n const from2 = parseInt(String(u.searchParams.get(\"from2\") || \"\").trim(), 10);\n const to2 = parseInt(String(u.searchParams.get(\"to2\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await compareGenesisReplayEquivalenceV1({\n from1,\n to1,\n from2,\n to2,\n type,\n includeCheckpoints\n });\n if (\n !out.ok &&\n (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")\n ) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayAnalytics) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayAnalyticsV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEvolution) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const collapseWindows = parseInt(String(u.searchParams.get(\"collapseWindows\") || \"0\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayEvolutionV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n collapseWindows,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/metrics\") {\n res.writeHead(200, { \"Content-Type\": \"text/plain; version=0.0.4; charset=utf-8\" });\n res.end(renderInfraMetricsProm());\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.infraPrometheusMetrics) {\n if (process.env.CASTLE_PROMETHEUS_METRICS !== \"1\") {\n sendJson(res, 404, { ok: false, error: \"prometheus_metrics_disabled\" });\n return;\n }\n res.writeHead(200, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(renderRhizohPrometheusMetrics());\n return;\n }\n if (req.method === \"GET\" && pathname.startsWith(\"/infra/traces/\")) {\n const traceId = decodeURIComponent(pathname.slice(\"/infra/traces/\".length));\n const item = await getTraceById(traceId);\n if (!item) return sendJson(res, 404, { ok: false, error: \"trace_not_found\" });\n return sendJson(res, 200, { ok: true, trace: item });\n }\n if (req.method === \"GET\" && pathname === \"/infra/traces\") {\n const u = new URL(String(req.url || \"/infra/traces\"), `http://${req.headers.host || \"localhost\"}`);\n const sessionId = String(u.searchParams.get(\"sessionId\") || \"\").trim();\n const items = sessionId ? await listTracesBySession(sessionId) : [];\n return sendJson(res, 200, { ok: true, sessionId, traces: items });\n }\n\n /** Presence room mesh (C): JOIN / LEAVE / SNAPSHOT / DELTA / REPLAY (HTTP); SUBSCRIBE = SSE stream. */\n if (req.method === \"POST\" && pathname === \"/presence/mesh/join\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid, clientUid: actor.clientUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/leave\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshLeave(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/snapshot\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n sendJson(res, 200, meshSnapshot(roomUid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/delta\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, MAX_MESSAGE_BYTES);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const seq = meshAppendDelta(roomUid, actor.clientUid, {\n node: body?.node,\n projectionPatch: body?.projectionPatch,\n writerSubject: body?.writerSubject\n });\n sendJson(res, 200, { ok: true, roomUid, seq });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/replay\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const fromNodeId = body?.fromNodeId != null ? String(body.fromNodeId) : \"\";\n const fromSeq = body?.fromSeq;\n if (fromNodeId) {\n sendJson(res, 200, meshReplayFromNodeId(roomUid, fromNodeId));\n return;\n }\n sendJson(res, 200, meshReplayFromSeq(roomUid, fromSeq));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/presence/mesh/subscribe\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const roomUid = String(q.searchParams.get(\"roomUid\") || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n const write = (chunk) => {\n try {\n res.write(chunk);\n } catch {\n /* closed */\n }\n };\n const unsub = meshSubscribeSse(roomUid, write);\n const onClose = () => {\n unsub();\n meshLeave(roomUid, actor.clientUid);\n try {\n res.end();\n } catch {\n /* ignore */\n }\n };\n req.on(\"close\", onClose);\n req.on(\"aborted\", onClose);\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, uid: auth.uid, mode: auth.mode, items: await listConnections(auth.uid) });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const id = await createConnection(auth.uid, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"create_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n await updateConnection(auth.uid, id, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"update_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"DELETE\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n await deleteConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"delete_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url?.startsWith(\"/rhizoh/network/presence\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const u = new URL(req.url, \"http://localhost\");\n const roomKey = String(u.searchParams.get(\"room\") || \"world_space_c2c_v0\").slice(0, 64);\n sendJson(res, 200, {\n ok: true,\n roomKey,\n presence: listCastleNetworkPresenceV0(roomKey)\n });\n } catch (error) {\n sendJson(res, 500, { ok: false, error: error?.message || \"presence_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/rhizoh/sync/vault\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const snapshot = await getCloudSyncSnapshotV0(auth.uid);\n sendJson(res, 200, { ok: true, uid: auth.uid, snapshot });\n } catch (error) {\n sendJson(res, 500, { ok: false, error: error?.message || \"cloud_sync_pull_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/rhizoh/sync/vault\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const snapshot = await mergeCloudSyncSnapshotV0(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, uid: auth.uid, snapshot });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"cloud_sync_push_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections/test\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n if (!checkHttpRateLimit(`llm_conn_test:${auth.uid}`, RL_LLM_CONN_TEST_PER_MIN, 60_000)) {\n return sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n }\n const hasBodyKey = !!String(payload?.apiKey || \"\").trim();\n const result = await queryRhizohLlm(\n {\n message: String(payload?.message || \"Ping from Castle Gateway\"),\n context: payload?.context || { source: \"connection_test\" },\n provider: payload?.provider,\n model: payload?.model,\n apiKey: payload?.apiKey || \"\"\n },\n { allowExternalApiKey: true, llmKeySource: hasBodyKey ? \"auto\" : \"env\" }\n );\n logLlmAccess({\n route: \"/llm/connections/test\",\n uid: auth.uid,\n llmKeyBillingOwner: result.llmKeyBillingOwner,\n llmKeyOrigin: result.llmKeyOrigin,\n provider: result.provider,\n model: result.model,\n connectionId: null\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"connection_test_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAgentIdentities(auth.uid);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerAgentIdentity({\n ownerUid: auth.uid,\n agentId: payload?.agentId,\n role: payload?.role,\n personaSeed: payload?.personaSeed || {},\n capabilityLevel: payload?.capabilityLevel\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/agents/identities/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const owns = await getAgentIdentity(id);\n if (!owns || owns.ownerUid !== auth.uid) return sendJson(res, 403, { ok: false, error: \"forbidden\" });\n const payload = await readHttpJson(req);\n const row = await updateAgentIdentity(id, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/academy/events\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/academy/events\"}`);\n const status = String(url.searchParams.get(\"status\") || \"\");\n const items = await listAcademyEvents({ ownerUid: auth.uid, status, limit: 120 });\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/academy/events\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const ev = await queueAcademyEvent({\n type: payload?.type,\n roomId: payload?.roomId,\n ownerUid: auth.uid,\n participants: payload?.participants || [],\n topic: payload?.topic || \"academy\",\n payload: payload?.payload || {}\n });\n sendJson(res, 200, { ok: true, event: ev });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && String(req.url || \"\").startsWith(\"/academy/events/\") && String(req.url || \"\").endsWith(\"/resolve\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const parts = String(req.url || \"\").split(\"/\").filter(Boolean);\n const eventId = parts[2] || \"\";\n try {\n const payload = await readHttpJson(req);\n const out = await resolveAcademyEvent(eventId, payload || {});\n sendJson(res, 200, { ok: true, event: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_resolve_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/memory\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/memory\"}`);\n const scope = url.searchParams.get(\"scope\") === \"agents\" ? \"agents\" : \"users\";\n const id = String(url.searchParams.get(\"id\") || (scope === \"agents\" ? \"\" : auth.uid));\n const kind = String(url.searchParams.get(\"kind\") || \"\");\n const limit = Number(url.searchParams.get(\"limit\") || 40);\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n sendJson(res, 200, { ok: true, scope, id, kind, items: await listMemories({ scope, id, kind, limit }) });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const profile = await getPersonaGoalMemory(auth.uid);\n sendJson(res, 200, { ok: true, profile });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const profile = await setPersonaGoalMemory({\n uid: auth.uid,\n personaPatch: payload?.personaPatch || {},\n goals: Array.isArray(payload?.goals) ? payload.goals : null,\n preferencesPatch: payload?.preferencesPatch || {}\n });\n sendJson(res, 200, { ok: true, profile });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"profile_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/compact\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const out = await autoCompactMemories({ scope, id });\n sendJson(res, 200, { ok: true, scope, id, ...out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_compact_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/context\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const context = await getMemoryContext({\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n query: String(payload?.query || \"\"),\n limit: Number(payload?.limit || 80)\n });\n sendJson(res, 200, { ok: true, context });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_context_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/place-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const placeName = String(payload?.placeName || \"\").trim();\n if (!placeName) return sendJson(res, 400, { ok: false, error: \"placeName_required\" });\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const persona = String(payload?.waypointPersona || payload?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(placeName)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Place: ${placeName}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Give a concise conversational briefing about this place for a walking-route companion.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-place-brief\", placeName, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `PLACE_BRIEF:${placeName} => ${String(summary.reply || \"\").slice(0, 900)}`,\n tags: [\"place\", \"brief\", \"event-layer\"],\n importance: 0.62,\n meta: { placeName }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"place-brief\",\n text: `${placeName} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: {\n placeName,\n directive: summary.directive,\n lat: hit?.lat ? Number(hit.lat) : null,\n lon: hit?.lon ? Number(hit.lon) : null\n }\n });\n sendJson(res, 200, { ok: true, place: hit || null, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"place_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/route-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const waypoints = Array.isArray(payload?.waypoints) ? payload.waypoints.slice(0, 24) : [];\n const out = [];\n for (const wp of waypoints) {\n const name = String(wp?.name || \"\").trim();\n if (!name) continue;\n const persona = String(wp?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(name)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Waypoint: ${name}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Generate a concise spoken briefing for route waypoint.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-route-brief\", waypoint: name, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n out.push({\n name,\n persona,\n place: hit || null,\n reply: summary.reply,\n directive: summary.directive\n });\n }\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `ROUTE_BRIEF: waypoints=${out.length} ${out.map((x) => `${x.name}:${x.persona}`).join(\" | \")}`.slice(0, 1300),\n tags: [\"route\", \"waypoint\", \"brief\", \"event-layer\"],\n importance: 0.74,\n meta: { count: out.length }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"route-brief\",\n text: out.map((x) => `${x.name}:${String(x.reply || \"\").slice(0, 140)}`).join(\" | \").slice(0, 1400),\n roomId: \"studio-main\",\n meta: {\n waypointCount: out.length,\n waypoints: out.map((x) => ({\n name: x.name,\n persona: x.persona,\n lat: x.place?.lat ? Number(x.place.lat) : null,\n lon: x.place?.lon ? Number(x.place.lon) : null\n }))\n }\n });\n sendJson(res, 200, { ok: true, items: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"route_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/pdf-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const title = String(payload?.title || \"document\");\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const providedText = String(payload?.text || \"\");\n const url = String(payload?.url || \"\");\n let content = providedText;\n if (!content && url) content = await fetchTextFromUrl(url);\n if (!content) return sendJson(res, 400, { ok: false, error: \"text_or_supported_url_required\" });\n const summary = await queryRhizohLlm(\n {\n message: `Summarize this document and provide key points + practical actions.\\nTitle:${title}\\nContent:\\n${content.slice(0, 12000)}`,\n context: { source: \"event-layer-pdf-brief\", title },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `DOC_BRIEF:${title} => ${String(summary.reply || \"\").slice(0, 1000)}`,\n tags: [\"document\", \"brief\", \"library\", \"event-layer\"],\n importance: 0.7,\n meta: { title, url: url || null }\n });\n await addTranscript(auth.uid, {\n source: \"library-brief\",\n eventType: \"pdf-brief\",\n text: `${title} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { title }\n });\n sendJson(res, 200, { ok: true, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/pdf/upload\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req, 8 * 1024 * 1024);\n const job = await enqueuePdfJob({\n uid: auth.uid,\n fileName: payload?.fileName || \"document.pdf\",\n bytesBase64: payload?.contentBase64\n });\n sendJson(res, 200, { ok: true, job });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_upload_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/session\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createPublishSession(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session\",\n text: `Publish session armed: ${row.protocol} ${row.target}`.slice(0, 500),\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: row.id, bridge: row.bridge }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/studio/publish/sessions\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listPublishSessions(auth.uid, 80);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/health-check\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const protocol = String(payload?.protocol || \"WHIP\");\n const target = String(payload?.target || \"\");\n const health = await checkPublishTargetHealth(protocol, target);\n if (payload?.sessionId) {\n await updatePublishSession(auth.uid, String(payload.sessionId), {\n status: health.ok ? \"healthy\" : \"degraded\",\n reconnectPolicy: {\n enabled: true,\n backoffSec: [2, 5, 10, 20],\n nextRetryInSec: health.reconnectInSec || 0,\n maxRetries: 8,\n reason: health.reason\n }\n });\n }\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-health\",\n text: `${protocol} ${target} => ${health.ok ? \"healthy\" : \"degraded\"} (${health.reason})`,\n roomId: \"studio-main\",\n meta: { protocol, target, reconnect: !!health.shouldReconnect }\n });\n sendJson(res, 200, { ok: true, health });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_health_check_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/studio/publish/sessions/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updatePublishSession(auth.uid, id, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session-update\",\n text: `Session ${id} -> ${row.status || \"updated\"}`,\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: id, status: row.status, ingestUrl: row.ingestUrl || \"\" }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/transcripts\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/transcripts\"}`);\n const limit = Math.max(1, Math.min(200, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await listTranscripts(auth.uid, limit);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/transcripts\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await addTranscript(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"transcript_write_failed\" });\n }\n return;\n }\n\n /** E1: broadcast intent → GreenRoom capability → transcript + memory artifact */\n if (req.method === \"POST\" && req.url === \"/studio/capabilities/greenroom\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const traceId = String(payload?.traceId || `TRC-${Date.now().toString(36).toUpperCase()}`);\n const title = String(payload?.title || \"Castle Live Broadcast\").slice(0, 200);\n const intentRaw = String(payload?.intentRaw || \"\").slice(0, 800);\n const audienceEstimate = Math.max(0, Math.min(50_000_000, Number(payload?.audienceEstimate) || 1200));\n const roomId = String(payload?.roomId || \"greenroom-main\");\n const transcriptRow = await addTranscript(auth.uid, {\n source: \"rhizoh\",\n eventType: \"BROADCAST_ROUTED\",\n text: `GreenRoom · ${title}${intentRaw ? `\\nIntent: ${intentRaw}` : \"\"}`,\n roomId,\n meta: {\n ack: \"BROADCAST_ROUTED\",\n capability: \"greenroom\",\n intentType: \"broadcast\",\n traceId,\n audienceEstimate,\n artifact: {\n kind: \"broadcast_card\",\n title,\n traceId,\n roomId,\n createdAt: Date.now()\n }\n }\n });\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n text: `GREENROOM_BROADCAST:${title} trace=${traceId}`,\n tags: [\"broadcast\", \"greenroom\", \"artifact\", \"castle-library\"],\n importance: 0.72,\n kind: \"episodic\",\n meta: { traceId, capability: \"greenroom\", transcriptId: transcriptRow.id }\n });\n sendJson(res, 200, {\n ok: true,\n ack: \"BROADCAST_ROUTED\",\n traceId,\n transcript: transcriptRow,\n replayPath: `/replay/${encodeURIComponent(traceId)}`,\n sharePath: `/replay/${encodeURIComponent(traceId)}`\n });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"greenroom_capability_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/metadata/search\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/metadata/search\"}`);\n const q = String(url.searchParams.get(\"q\") || \"\");\n const limit = Math.max(1, Math.min(300, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await searchMetadata(auth.uid, q, limit);\n sendJson(res, 200, { ok: true, q, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerDevice(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, device: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"device_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const lat = Number(payload?.lat);\n const lon = Number(payload?.lon);\n const speed = Math.max(0, Number(payload?.speed || 0));\n const cfgCenterLat = Number(process.env.CASTLE_GEOFENCE_CENTER_LAT || 41.0082);\n const cfgCenterLon = Number(process.env.CASTLE_GEOFENCE_CENTER_LON || 28.9784);\n const cfgRadiusM = Math.max(100, Number(process.env.CASTLE_GEOFENCE_RADIUS_M || 55000));\n const cfgSpeedCap = Math.max(1, Number(process.env.CASTLE_SPEED_CAP_MPS || 35));\n const dist = Number.isFinite(lat) && Number.isFinite(lon) ? haversineMeters(cfgCenterLat, cfgCenterLon, lat, lon) : 0;\n const geofenceOk = !Number.isFinite(dist) ? true : dist <= cfgRadiusM;\n const speedOk = speed <= cfgSpeedCap;\n const failsafe = !(geofenceOk && speedOk);\n const safety = {\n geofenceOk,\n speedOk,\n failsafe,\n distanceFromCenterM: Number.isFinite(dist) ? dist : null,\n speedCapMps: cfgSpeedCap\n };\n const row = await appendTelemetry(auth.uid, deviceId, {\n lat,\n lon,\n speed,\n battery: payload?.battery,\n mode: payload?.mode || (failsafe ? \"FAILSAFE_HOLD\" : \"ACTIVE\"),\n raw: payload?.raw || {}\n });\n await updateDevice(auth.uid, deviceId, {\n status: failsafe ? \"failsafe\" : \"active\",\n lastTelemetryAt: row.ts,\n safetyState: safety\n });\n if (failsafe) {\n await addTranscript(auth.uid, {\n source: \"robotics-guardrails\",\n eventType: \"failsafe\",\n text: `Device ${deviceId} failsafe triggered geofenceOk=${geofenceOk} speedOk=${speedOk}`,\n roomId: \"studio-main\",\n meta: { deviceId, safety }\n });\n }\n sendJson(res, 200, { ok: true, telemetry: row, safety, command: failsafe ? \"FAILSAFE_HOLD\" : \"CONTINUE\" });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"telemetry_ingest_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listTelemetry(auth.uid, 150);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createAutomation(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"automation-scheduled\",\n text: `Automation scheduled: ${row.title} at ${new Date(row.scheduleAt).toLocaleString()}`,\n roomId: \"studio-main\",\n meta: { automationId: row.id, action: row.action, deviceId: row.deviceId }\n });\n sendJson(res, 200, { ok: true, automation: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"automation_create_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAutomations(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listDevices(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const devs = await listDevices(auth.uid, 500);\n const d = devs.find((x) => x.id === deviceId);\n if (!d) return sendJson(res, 404, { ok: false, error: \"device_not_found\" });\n const action = String(payload?.action || \"\");\n const caps = Array.isArray(d.capabilityProfile) ? d.capabilityProfile : [];\n if (action && caps.length && !caps.includes(action) && !caps.includes(\"*\")) {\n return sendJson(res, 400, { ok: false, error: \"unsupported_action_for_device\", capabilityProfile: caps });\n }\n const cmd = await enqueueCommand(auth.uid, {\n deviceId,\n action: action || \"NOOP\",\n params: payload?.params || {},\n adapter: payload?.adapter || d.adapter || \"websocket\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-command-queue\",\n eventType: \"command-queued\",\n text: `${cmd.deviceId} -> ${cmd.action}`,\n roomId: \"studio-main\",\n meta: { commandId: cmd.id, adapter: cmd.adapter }\n });\n sendJson(res, 200, { ok: true, command: cmd });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listCommands(auth.uid, 200);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/robotics/commands/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updateCommandStatus(auth.uid, id, payload?.status || \"sent\");\n sendJson(res, 200, { ok: true, command: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/rhizoh/bridge\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const result = await runRhizohBrainV2({\n message: String(payload?.message || \"robotics bridge command\"),\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n worldState: payload?.worldState || {},\n deviceState: payload?.deviceState || {},\n intent: \"robotics-mechanics\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-bridge\",\n eventType: \"robotics-command\",\n text: `Bridge -> ${String(result.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { directive: result.directive, agentId: payload?.agentId || \"\" }\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"robotics_bridge_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/planner/graph\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const graph = buildTaskActionGraph(String(payload?.task || \"\"), payload?.context || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"planner-graph\",\n text: `Planner graph built: ${graph.taskTitle} nodes=${graph.nodes.length}`,\n roomId: \"studio-main\",\n meta: { taskTitle: graph.taskTitle, nodeCount: graph.nodes.length }\n });\n sendJson(res, 200, { ok: true, graph });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"planner_graph_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerSocialChannel(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, channel: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_channel_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listSocialChannels(auth.uid, 100);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/broadcast\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const text = String(payload?.text || \"\").slice(0, 1200);\n const channels = await listSocialChannels(auth.uid, 100);\n const enabled = channels.filter((c) => c.status === \"enabled\");\n const deliveries = [];\n for (const c of enabled) {\n const out = await deliverSocialWithRetry(c, text);\n deliveries.push({ id: c.id, platform: c.platform, ...out });\n }\n const okCount = deliveries.filter((d) => d.ok).length;\n await addTranscript(auth.uid, {\n source: \"social-layer\",\n eventType: \"social-broadcast\",\n text: `Broadcast delivered ${okCount}/${enabled.length} channel(s): ${text.slice(0, 240)}`,\n roomId: \"studio-main\",\n meta: { deliveries }\n });\n sendJson(res, 200, { ok: true, sentChannels: okCount, totalChannels: enabled.length, deliveries });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_broadcast_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/ethics/programs\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, ...buildEthicsPrograms() });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands/dispatch\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const all = await listCommands(auth.uid, 200);\n const queued = all.filter((c) => String(c.status || \"queued\") === \"queued\").slice(0, 50);\n const devices = await listDevices(auth.uid, 500);\n const byId = new Map(devices.map((d) => [d.id, d]));\n const results = [];\n for (const cmd of queued) {\n const dev = byId.get(cmd.deviceId);\n if (!dev) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: \"device_not_found\" });\n continue;\n }\n try {\n const dispatched = await dispatchCommandToAdapter(dev, cmd);\n await updateCommandStatus(auth.uid, cmd.id, \"sent\");\n results.push({ id: cmd.id, ok: true, adapter: dispatched.adapter, via: dispatched.via });\n } catch (error) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: String(error?.message || \"dispatch_failed\") });\n }\n }\n await addTranscript(auth.uid, {\n source: \"robotics-command-dispatch\",\n eventType: \"command-dispatch-batch\",\n text: `Dispatch batch size=${results.length} ok=${results.filter((x) => x.ok).length}`,\n roomId: \"studio-main\",\n meta: { results: results.slice(0, 30) }\n });\n sendJson(res, 200, { ok: true, processed: results.length, results });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"dispatch_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/pdf/jobs/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n const job = await getPdfJob(auth.uid, id);\n if (!job) return sendJson(res, 404, { ok: false, error: \"pdf_job_not_found\" });\n sendJson(res, 200, { ok: true, job });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const row = await appendMemory({\n scope,\n id,\n text: payload?.text,\n tags: payload?.tags || [],\n importance: payload?.importance,\n kind: payload?.kind || \"episodic\",\n meta: { uid: auth.uid, ...(payload?.meta || {}) }\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.externalTruth) {\n try {\n if (process.env.RHIZOH_EXTERNAL_TRUTH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_truth:${ip}`, RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n sendJson(res, 200, buildRhizohExternalGroundTruthPayload(req));\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.productOutcome) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const rawBody = await readHttpBodyText(req, 64 * 1024);\n const body = rawBody.trim() ? safeJsonParse(rawBody) : null;\n if (!body || typeof body !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"invalid_json_payload\" });\n return;\n }\n const sigHdr = String(req.headers?.[\"x-rhizoh-outcome-signature\"] || \"\").trim();\n const hmacV = verifyRhizohOutcomeHmac(rawBody, sigHdr);\n if (!hmacV.ok) {\n sendJson(res, 401, { ok: false, error: hmacV.error || \"outcome_hmac_invalid\" });\n return;\n }\n const srcTokHdr = String(req.headers?.[\"x-rhizoh-outcome-source-token\"] || \"\").trim();\n const srcV = verifyRhizohOutcomeSourceToken(body.source, srcTokHdr);\n if (!srcV.ok) {\n sendJson(res, 401, { ok: false, error: srcV.error || \"outcome_source_token_invalid\" });\n return;\n }\n const subjectKey = String(body.subjectKey || \"\").trim().slice(0, 128);\n if (\n subjectKey &&\n !checkHttpRateLimit(\n `rhizoh_product_outcome_subj:${subjectKey}`,\n RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN,\n 60_000\n )\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_subject\" });\n return;\n }\n const result = ingestRhizohProductOutcomeHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n schemaVersion: \"1.1.0\",\n aggregate: result.aggregate,\n integrity: result.integrity\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"outcome_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.productOutcomeAggregate) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome_agg:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const u = new URL(String(req.url || \"/\"), \"http://localhost\");\n const cohortId = u.searchParams.get(\"cohortId\") || \"\";\n const decisionFingerprint = u.searchParams.get(\"decisionFingerprint\") || \"\";\n const limit = u.searchParams.get(\"limit\") || \"50\";\n const rows = listRhizohProductOutcomeAggregates({\n cohortId: cohortId || undefined,\n decisionFingerprint: decisionFingerprint || undefined,\n limit: Number(limit) || 50\n });\n sendJson(res, 200, { ok: true, schemaVersion: \"1.1.0\", rows });\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.externalLossBatch) {\n try {\n if (process.env.RHIZOH_EXTERNAL_LOSS_BATCH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_loss_batch:${ip}`, RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const result = ingestRhizohExternalLossBatchHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, { ok: true, accepted: result.accepted ?? 0 });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"batch_failed\") });\n }\n return;\n }\n\n function moderationAdminOk(req) {\n const expected = String(process.env.CASTLE_MODERATION_ADMIN_KEY || \"\").trim();\n if (!expected) return false;\n const got = String(req.headers[\"x-castle-moderation-key\"] || \"\").trim();\n return got.length > 0 && got === expected;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.opsAbuseReport) {\n try {\n const body = await readHttpJson(req, 32 * 1024);\n const auth = await resolveHttpUser(req);\n const entry = submitAbuseReportV0({\n reporterUid: auth.ok ? auth.uid : null,\n targetUid: body?.targetUid ?? null,\n category: body?.category,\n detail: body?.detail,\n traceId: body?.traceId ?? null\n });\n sendJson(res, 202, { ok: true, reportId: entry.id, status: entry.status });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsModerationQueue) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n queue: listModerationQueueV0(100),\n softBlocked: listSoftBlockedUsersV0()\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-block\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const out = softBlockUserV0(body?.uid, body?.reason);\n sendJson(res, out.ok ? 200 : 400, out);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-unblock\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n sendJson(res, 200, removeSoftBlockV0(body?.uid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsAgentSnapshots) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n const limit = Number(new URL(req.url || \"\", \"http://local\").searchParams.get(\"limit\") || 50);\n sendJson(res, 200, { ok: true, snapshots: listRecentAgentSnapshotsV0(limit) });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.academicObservatoryExport) {\n const auth = await resolveHttpUser(req);\n const out = handleAcademicObservatoryExportGetV0(req, auth);\n sendJson(res, out.status, out.body);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsHardeningStatus) {\n const auth = await resolveHttpUser(req);\n const principal = auth.ok ? `uid:${auth.uid}` : null;\n const gclSnap = await getGlobalCostLedgerSnapshotV0(principal);\n const economicStrategy = evaluateEconomicStrategyL3V0(gclSnap);\n const hardeningTenantId = resolveHardeningTenantIdV0(auth, req);\n const unifiedState = await buildUnifiedStateNarrativeV0({\n tenantId: hardeningTenantId ?? undefined,\n platformScope: !hardeningTenantId,\n principal\n });\n sendJson(res, 200, {\n ok: true,\n schema: \"rhizoh.ops.hardening_status.v0\",\n containment: readAgentContainmentConfigV0(),\n cost: readCostContainmentConfigV0(),\n gcl: {\n backend: await getGclBackendStatusV0(),\n snapshot: gclSnap\n },\n economicStrategyL3: economicStrategy,\n phasedRollout: await getPhasedRolloutStatsV0(),\n tier: readPhasedRolloutTierV0(),\n principalCost: auth.ok ? await getCostContainmentStatsV0(principal) : null,\n stressTaxonomy: verifyStressTaxonomyCoverageV0(),\n stressConflictResolution: verifyStressConflictResolutionV0(),\n resolutionStabilityEnvelope: verifyResolutionStabilityEnvelopeV0(),\n unifiedState: {\n tenantScope: {\n tenantId: unifiedState.tenantScope?.tenantId,\n scope: unifiedState.tenantScope?.scope,\n noGlobalDerivedState: unifiedState.tenantScope?.noGlobalDerivedState\n },\n narrativeFingerprint: unifiedState.narrativeFingerprint,\n screenshotScopeWatermark: unifiedState.screenshotScopeWatermark,\n interpretationSafetyContract: unifiedState.interpretationSafetyContract,\n interpretationUxContract: {\n schema: unifiedState.interpretationUxContract?.schema,\n layerOrder: unifiedState.interpretationUxContract?.layerOrder,\n narrativeNeverVisuallyDominant: unifiedState.interpretationUxContract?.narrativeNeverVisuallyDominant\n },\n governance: {\n narrativeIsNotDecisionLayer: unifiedState.governance.narrativeIsNotDecisionLayer,\n displayOrder: unifiedState.governance.displayOrder,\n trustPosture: unifiedState.governance.trustPosture,\n interpretationUxContract: unifiedState.governance.interpretationUxContract?.schema\n },\n humanOps: {\n decisionTier: unifiedState.humanOps?.humanDecisionScaling?.tier,\n decisionOwner: unifiedState.humanOps?.humanDecisionScaling?.decisionOwner,\n opsRouteId: unifiedState.humanOps?.humanDecisionOpsRunbook?.routing?.routeId,\n slaAckMinutes: unifiedState.humanOps?.humanDecisionOpsRunbook?.sla?.ackMinutes,\n uiDisclaimerRequired: unifiedState.humanOps?.narrativeUiSafety?.displayRules?.requireDisclaimerBanner,\n misreadHighResidual: unifiedState.humanOps?.misreadSimulation?.highResidualCount,\n propagationHighResidual: unifiedState.humanOps?.socialPropagationSimulation?.highResidualCount,\n worstWatermarkSurvivability:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.worstWatermarkSurvivability,\n dominantDistortionSource:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.dominantDistortionSource\n },\n culturalRisk: {\n perceptualRiskClass: unifiedState.culturalRisk?.perceptualRiskClass,\n trustFork: unifiedState.culturalRisk?.trustDynamics?.fork,\n trustDecayScore: unifiedState.culturalRisk?.trustDynamics?.scores?.trustDecay,\n mythologyScore: unifiedState.culturalRisk?.trustDynamics?.scores?.mythology,\n confidenceDetachmentExposure:\n unifiedState.culturalRisk?.trustDynamics?.axes?.confidenceDetachmentExposure\n },\n appliedSystemsLayer: {\n schema: unifiedState.appliedSystemsLayer?.schema,\n researchOutputType: unifiedState.appliedSystemsLayer?.researchGovernance?.outputType,\n roboticsBlockActuation: unifiedState.appliedSystemsLayer?.roboticsGrounding?.blockActuation,\n spiralRumorCount:\n unifiedState.appliedSystemsLayer?.spiralMMOGameKernel?.gameLayer?.rumors?.length\n },\n actionSemanticGovernance: {\n schema: unifiedState.actionSemanticGovernance?.schema,\n highRiskActionCount: unifiedState.actionSemanticGovernance?.highRiskActionCount,\n loopClosed: unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.loopClosed,\n authorityCollapseMitigated:\n unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.risks?.authorityCollapse\n ?.mitigated\n },\n actionContextResolution: {\n schema: unifiedState.actionContextResolution?.schema,\n contextFingerprint: unifiedState.actionContextResolution?.contextFingerprint?.fingerprintHash,\n domainCollision: unifiedState.actionContextResolution?.domainCollision?.detected,\n deployAgentSpiralBlocked:\n unifiedState.actionContextResolution?.actionOverloadingRisk?.deployAgentExample\n ?.spiral_mmo === \"blocked\",\n roboticsActuationEligible:\n unifiedState.actionContextResolution?.roboticsActuationCorrectness?.actuationEligible\n },\n epistemicCoherence: {\n schema: unifiedState.epistemicCoherence?.schema,\n verdict: unifiedState.epistemicCoherence?.systemCoherence?.verdict,\n coherenceScore: unifiedState.epistemicCoherence?.systemCoherence?.score,\n contradictionCount: unifiedState.epistemicCoherence?.crossLayerContradictions?.count,\n operatorDecision: unifiedState.epistemicCoherence?.uxCompression?.operatorDecision,\n headlineTr: unifiedState.epistemicCoherence?.uxCompression?.headline?.tr,\n falseSafetyWarning: Boolean(\n unifiedState.epistemicCoherence?.uxCompression?.falseSafetyWarning\n )\n },\n coherenceAuthorityBoundary: {\n schema: unifiedState.coherenceAuthorityBoundary?.schema,\n bindingValid: unifiedState.coherenceAuthorityBoundary?.eclAuthorityBinding?.valid,\n canExecute: unifiedState.coherenceAuthorityBoundary?.contract?.can_execute,\n mandatoryBannerTr:\n unifiedState.coherenceAuthorityBoundary?.disclaimers?.mandatoryBanner?.tr\n },\n decisionLatencyGovernance: {\n schema: unifiedState.decisionLatencyGovernance?.schema,\n latencyTier: unifiedState.decisionLatencyGovernance?.latencyTier,\n inflationScore: unifiedState.decisionLatencyGovernance?.latencyInflation?.score,\n fastPathEligible: unifiedState.decisionLatencyGovernance?.fastPath?.eligible,\n primaryActionId: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.primaryActionId,\n ackSlaMinutes: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.ackSlaMinutes,\n certaintyCap: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.certaintyCap,\n uncertaintyItems:\n unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.uncertaintyEnvelope?.itemCount\n },\n decisionPacketUncertaintyBoundary: {\n schema: unifiedState.decisionPacketUncertaintyBoundary?.schema,\n invariantValid: unifiedState.decisionPacketUncertaintyBoundary?.invariantCheck?.valid,\n centralInvariant: unifiedState.decisionPacketUncertaintyBoundary?.centralInvariant\n },\n epistemicDecisionPacing: {\n schema: unifiedState.epistemicDecisionPacing?.schema,\n queueSaturationTier:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation?.tier,\n operatorProcessingLatency:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation\n ?.operatorProcessingLatency,\n postureWindowCount:\n unifiedState.epistemicDecisionPacing?.temporaryStaticPostureWindows\n ?.temporaryStaticPostureWindows?.windowCount,\n executionBoundariesValid:\n unifiedState.epistemicDecisionPacing?.executionBoundaries?.valid,\n grantsExecutionApproval: unifiedState.epistemicDecisionPacing?.grantsExecutionApproval\n },\n epistemicTemporalCoherence: {\n schema: unifiedState.epistemicTemporalCoherence?.schema,\n allInvariantsValid: unifiedState.epistemicTemporalCoherence?.allInvariantsValid,\n temporalAlignmentValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.temporalAlignment?.valid,\n crossWindowGuardValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard?.valid,\n delayedTruthValid: unifiedState.epistemicTemporalCoherence?.invariants?.delayedTruthRule?.valid,\n reconciliationRequired:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard\n ?.reconciliation?.required\n },\n realityDriftObserver: {\n schema: unifiedState.realityDriftObserver?.schema,\n driftActive: unifiedState.realityDriftObserver?.driftObserverSummary?.active,\n misapprehensionShapeCount:\n unifiedState.realityDriftObserver?.misapprehensionShapeCatalog?.shapeCount,\n criticalShapeCount:\n unifiedState.realityDriftObserver?.driftObserverSummary?.criticalShapeCount,\n roboticsMismatchActive:\n unifiedState.realityDriftObserver?.roboticsFeedbackMismatch?.active\n },\n driftCausality: {\n schema: unifiedState.driftCausality?.schema,\n shapesWithCausalExplanation: unifiedState.driftCausality?.rdolLinkage?.shapesWithCausalExplanation,\n activeDomainCount: unifiedState.driftCausality?.causalitySummary?.activeDomainCount,\n propagationCausalityActive: unifiedState.driftCausality?.causalDomains?.propagation?.active\n },\n productReadiness: unifiedState.productReadiness,\n stateLayers: {\n raw: unifiedState.stateLayers.raw,\n derived: {\n systemState: unifiedState.stateLayers.derived.systemState,\n narrative: unifiedState.stateLayers.derived.narrative,\n validation: unifiedState.stateLayers.derived.validation\n },\n policy: {\n binding: unifiedState.stateLayers.policy.binding,\n decisionOwner: unifiedState.stateLayers.policy.decisionOwner,\n prohibitedActions: unifiedState.stateLayers.policy.prohibitedActions\n }\n },\n systemState: unifiedState.systemState,\n interpretation: unifiedState.interpretation,\n validation: {\n trustPosture: unifiedState.validation.trustPosture,\n adjustedConfidence: unifiedState.validation.adjustedConfidence,\n agreement: unifiedState.validation.confidenceDecomposition.agreement,\n divergenceScore: unifiedState.validation.divergence.divergenceScore,\n validated: unifiedState.validation.divergence.validated,\n uncertaintyTags: unifiedState.validation.uncertainty.tags.map((t) => t.id),\n flags: unifiedState.validation.divergence.flags.map((f) => f.id)\n },\n compression: unifiedState.compression\n }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 6 * 1024 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (\n !checkHttpRateLimit(`rhizoh_voice_transcribe:${rlKey}`, RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN, 60_000)\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const voiceEnv = rhizohVoiceTranscribeEnvV3();\n if (!voiceEnv.any) {\n sendJson(res, 503, {\n ok: false,\n error: \"voice_asr_not_configured\",\n env: voiceEnv\n });\n return;\n }\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n const result = await runRhizohVoiceTranscribeV3(\n applyLanguagePropagationToVoiceBodyV1(payload, langProp)\n );\n sendJson(res, result.ok ? 200 : 422, {\n ...result,\n traceId: String(payload?.traceId || \"\"),\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n atMs: Date.now()\n });\n } catch (error) {\n sendJson(res, 500, {\n ok: false,\n error: \"voice_transcribe_failed\",\n detail: String(error?.message || error),\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(\"/rhizoh/llm/task/\")) {\n try {\n const rawTaskId = decodeURIComponent(pathname.slice(\"/rhizoh/llm/task/\".length).split(\"/\")[0] || \"\");\n const taskId = String(rawTaskId || \"\").toLowerCase() === \"last\" ? getLatestLlmWorkerTaskIdV0() : rawTaskId;\n const snap = taskId ? getLlmWorkerTaskSnapshotV0(taskId) : null;\n if (!snap) {\n sendJson(res, 404, {\n ok: false,\n error: \"llm_task_not_found\",\n taskId: rawTaskId,\n status: \"missing\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n if (snap.status === \"processing\") {\n sendJson(res, 202, {\n ok: true,\n taskId: snap.taskId,\n status: \"processing\",\n createdAt: snap.createdAt,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n // Terminal states (completed/failed) always HTTP 200 — poll must not look like server fault (500).\n sendJson(res, 200, {\n ...(snap.body || { ok: false, error: \"llm_task_empty\", taskId: snap.taskId }),\n taskId: snap.taskId,\n status: snap.status === \"completed\" ? \"completed\" : snap.status,\n terminal: true,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n } catch (error) {\n console.error(\"[llm-task-poll] handler failed:\", error);\n sendJson(res, 200, {\n ok: false,\n error: \"llm_task_poll_handler_failed\",\n detail: String(error?.message || error),\n status: \"failed\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === rhizohRuntime.routes.rhizohLlm) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 128 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n logRhizohHealth(\"gateway_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: String(payload?.traceId || \"\"),\n auth: auth.ok ? \"ok\" : \"anon\",\n ip\n });\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (!checkHttpRateLimit(`rhizoh_llm:${rlKey}`, RL_RHIZOH_LLM_PER_MIN, 60_000)) {\n const rlTaxonomy = classifyStressResponseV0({ code: \"rate_limit_exceeded\" });\n return sendJson(res, 429, {\n ok: false,\n error: \"rate_limit_exceeded\",\n reply: rlTaxonomy.userMessageTr || \"İstek sınırı aşıldı. Kısa süre sonra tekrar deneyin.\",\n stressClass: rlTaxonomy.stressClass,\n responseAction: rlTaxonomy.responseAction,\n stressMatrix: rlTaxonomy.matrix,\n stressInterpretable: rlTaxonomy.interpretable,\n stressConfidence: rlTaxonomy.stressConfidence,\n actionConfidence: rlTaxonomy.actionConfidence,\n actionSoftened: rlTaxonomy.actionSoftened,\n responseActionStrict: rlTaxonomy.responseActionStrict,\n actionInterpretable: rlTaxonomy.actionInterpretable,\n conflictResolution: rlTaxonomy.conflictResolution,\n directive: \"NONE\"\n });\n }\n\n const requireExplicit = process.env.CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE === \"1\";\n const keyMode = normalizeClientLlmKeySource(payload?.llmKeySource ?? payload?.keySource);\n if (keyMode === null) {\n return sendJson(res, 400, {\n ok: false,\n error: \"invalid_llm_key_source\",\n reply: \"llmKeySource geçersiz. Kullanın: env | user_connection | auto\",\n directive: \"NONE\"\n });\n }\n if (requireExplicit && keyMode === \"auto\") {\n return sendJson(res, 400, {\n ok: false,\n error: \"llm_key_source_required\",\n reply: \"Sunucu CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE=1: istekte llmKeySource (env veya user_connection) zorunlu.\",\n directive: \"NONE\"\n });\n }\n\n const conn = auth.ok ? await resolveConnection(auth.uid, String(payload?.connectionId || \"\")) : null;\n\n if (keyMode === \"user_connection\") {\n if (!auth.ok) {\n return sendJson(res, 401, {\n ok: false,\n error: \"llm_auth_required_for_user_connection\",\n reply: \"Kayıtlı LLM bağlantısı için giriş gerekli.\",\n directive: \"NONE\"\n });\n }\n if (!conn?.apiKey) {\n return sendJson(res, 400, {\n ok: false,\n error: \"user_llm_connection_required\",\n reply: \"Geçerli bir LLM bağlantısı yok; Studio’da bağlantı ekleyin veya llmKeySource=env kullanın.\",\n directive: \"NONE\"\n });\n }\n }\n\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n let safePayload = applyLanguagePropagationToLlmPayloadV1({ ...(payload || {}) }, langProp);\n const trimOut = trimRhizohLlmGatewayPayloadV1(safePayload);\n if (trimOut.meta.trimmed) {\n logRhizohHealth(\"llm_payload_trimmed\", {\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n bytesBefore: trimOut.meta.bytesBefore,\n bytesAfter: trimOut.meta.bytesAfter\n });\n }\n safePayload = trimOut.payload;\n delete safePayload.apiKey;\n delete safePayload.llmKeySource;\n delete safePayload.keySource;\n\n const llmMessage = String(\n safePayload?.message || safePayload?.text || payload?.message || payload?.text || \"\"\n ).trim();\n if (!llmMessage) {\n return sendJson(res, 400, {\n ok: false,\n error: \"message_required\",\n reply: \"Mesaj metni gerekli (message alanı boş).\",\n directive: \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n if (!safePayload.message) {\n safePayload = { ...safePayload, message: llmMessage };\n }\n\n let resolvedProvider = payload?.provider;\n let resolvedModel = payload?.model;\n let connApiKey = \"\";\n\n ({\n resolvedProvider,\n resolvedModel,\n connApiKey\n } = resolveServerLlmProviderV0(keyMode, conn, payload));\n\n const turnInput = {\n safePayload,\n auth,\n keyMode,\n conn,\n resolvedProvider,\n resolvedModel,\n connApiKey\n };\n const turnMeta = {\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n langEcho: buildRhizohLanguagePropagationEchoV1(langProp)\n };\n\n if (\n isLlmWorkerEnabledV0() &&\n isLlmWorkerAsyncEnabledV0() &&\n String(req.headers[\"x-rhizoh-llm-sync\"] || \"\").trim() !== \"1\"\n ) {\n const taskId = enqueueLlmWorkerTaskAsyncV0(turnInput, turnMeta);\n logRhizohHealth(\"llm_worker_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n taskId,\n traceId: String(payload?.traceId || taskId),\n keyMode,\n async: true\n });\n sendJson(res, 202, {\n ok: true,\n taskId,\n status: \"processing\",\n traceId: String(payload?.traceId || taskId),\n pollPath: `/rhizoh/llm/task/${taskId}`,\n gatewayPid: process.pid,\n ...turnMeta.langEcho\n });\n return;\n }\n\n const { result, traceId, turnLatencyMs, spinePhases, sampledTrace } = isLlmWorkerEnabledV0()\n ? await runLlmWorkerTaskV0(turnInput, turnMeta)\n : await rhizohGatewayTurn(turnInput);\n logRhizohHealth(\"llm_response\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId,\n turnLatencyMs: Number(turnLatencyMs || 0),\n keyMode\n });\n sendJson(res, 200, {\n ...result,\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n traceId,\n turnLatencyMs,\n ...(spinePhases ? { spinePhases } : {}),\n ...(sampledTrace !== undefined ? { sampledTrace } : {})\n });\n } catch (error) {\n const msg = String(error?.message || \"\");\n const code = error?.code || \"\";\n let status = 500;\n if (msg === \"payload_too_large\") status = 413;\n else if (msg === \"rate_limit_exceeded\") status = 429;\n else if (\n code === \"cost_hard_limit\" ||\n code === \"phased_rollout_capacity\" ||\n error?.containment === true ||\n String(code).startsWith(\"agent_\")\n ) {\n status = 429;\n } else if (code === \"user_soft_blocked\" || code === \"prompt_abuse_detected\") status = 403;\n else if (\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(code) ||\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(msg) ||\n msg.includes(\"missing_api_key_for_\")\n )\n status = 400;\n let rhizohFailureKind = \"provider_error\";\n if (msg === \"rate_limit_exceeded\" || status === 429) rhizohFailureKind = \"rate_limit\";\n else if (status === 403) rhizohFailureKind = \"policy_block\";\n else if (status === 400 || status === 413) rhizohFailureKind = \"client_config\";\n else if (msg.startsWith(\"provider_http_\")) {\n const st = Number(msg.replace(\"provider_http_\", \"\"));\n if (st === 408 || st === 504) rhizohFailureKind = \"timeout\";\n else rhizohFailureKind = \"provider_error\";\n } else if (/timeout|timed out|ETIMEDOUT/i.test(msg)) rhizohFailureKind = \"timeout\";\n\n const providerHttpMatch = msg.match(/^provider_http_(\\d{3})$/);\n const stressTaxonomy =\n error?.stressClass != null\n ? {\n stressClass: error.stressClass,\n responseAction: error.responseAction,\n stressMatrix: error.stressMatrix,\n stressInterpretable: error.stressInterpretable !== false,\n stressConfidence: error.stressConfidence,\n actionConfidence: error.actionConfidence,\n actionSoftened: error.actionSoftened,\n responseActionStrict: error.responseActionStrict,\n actionInterpretable: error.actionInterpretable,\n conflictResolution: error.conflictResolution,\n stressSecondary: error.stressSecondary,\n responseActions: error.responseActions\n }\n : (() => {\n const t = classifyStressResponseV0({\n code: code || msg,\n rhizohFailureKind,\n providerHttpStatus: providerHttpMatch\n ? Number(providerHttpMatch[1])\n : null\n });\n return {\n stressClass: t.stressClass,\n responseAction: t.responseAction,\n stressMatrix: t.matrix,\n stressInterpretable: t.interpretable,\n stressConfidence: t.stressConfidence,\n actionConfidence: t.actionConfidence,\n actionSoftened: t.actionSoftened,\n responseActionStrict: t.responseActionStrict,\n actionInterpretable: t.actionInterpretable,\n conflictResolution: t.conflictResolution,\n stressSecondary: t.stressSecondary,\n responseActions: t.responseActions\n };\n })();\n sendJson(res, status, {\n ok: false,\n rhizohFailureKind,\n ...(providerHttpMatch ? { providerHttpStatus: Number(providerHttpMatch[1]) } : {}),\n error: code || msg || \"rhizoh_llm_failed\",\n detail: msg || code || undefined,\n reply:\n error?.reply ||\n (status === 500 ? \"Rhizoh bağlantısı geçici olarak kesildi.\" : msg),\n directive: error?.directive || \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n ...stressTaxonomy\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicSeal) {\n try {\n if (!EPISTEMIC_SEAL_SECRET) {\n sendJson(res, 503, { ok: false, error: \"epistemic_seal_disabled\" });\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`epistemic_seal:${ip}`, RL_EPISTEMIC_SEAL_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const truth = body.truth_contract;\n if (!truth || typeof truth !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"truth_contract_required\" });\n return;\n }\n const sealBody = {\n truth_contract: truth,\n runtime_hash: String(body.runtime_hash || \"\"),\n model_route: {\n provider: body.model_route?.provider ?? null,\n model: body.model_route?.model ?? null\n },\n memory_digest: String(body.memory_digest || \"\"),\n world_snapshot_hash: String(body.world_snapshot_hash || \"\"),\n timestamp: Number(body.timestamp) || Date.now()\n };\n const canonical = canonicalEpistemicSealString(sealBody);\n const { hash, signature } = hashAndSignEpistemicSeal(canonical, EPISTEMIC_SEAL_SECRET);\n recordGenesisEpistemicSealIssued(hash);\n const attestation = {\n schema: EPISTEMIC_SEAL_SCHEMA,\n algorithm: \"SHA-256+HMAC-SHA256\",\n keyHint: process.env.CASTLE_EPISTEMIC_SEAL_SECRET ? \"CASTLE_EPISTEMIC_SEAL_SECRET\" : \"CASTLE_GATEWAY_TOKEN\",\n issuedAt: Date.now(),\n gateway: { service: \"castle-gateway\", listenPort: PORT }\n };\n sendJson(res, 200, {\n ok: true,\n hash,\n signature,\n attestation,\n canonicalBytes: Buffer.byteLength(canonical, \"utf8\")\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"seal_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicLogsBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`epistemic_logs:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 120) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const persisted = await persistEpistemicLedgerBatch(auth.uid, entries);\n recordGenesisEpistemicLedgerPersisted(persisted.persisted);\n // Internal cognition layer: forecast stays gateway/ledger-only (no client exposure).\n try {\n await persistEpistemicForecastBatch(auth.uid, persisted.normalized || []);\n } catch {\n /* forecast failures must not break primary ledger ingest */\n }\n sendJson(res, 200, {\n ok: true,\n persisted: persisted.persisted,\n mode: persisted.mode,\n latest: persisted.normalized?.[persisted.normalized.length - 1] || null\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"epistemic_log_ingest_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.authorityLedgerBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`authority_ledger:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 80) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const witnessed = persistAuthorityLedgerWitnessBatchV1(auth.uid, entries, EPISTEMIC_SEAL_SECRET);\n if (witnessed.witnessed > 0) {\n recordGenesisEpistemicLedgerPersisted(witnessed.witnessed);\n const clientSeal = witnessed.lastWitness?.clientSealHash;\n if (clientSeal) recordGenesisEpistemicSealIssued(clientSeal);\n }\n sendJson(res, 200, {\n ok: true,\n witnessed: witnessed.witnessed,\n quarantined: witnessed.quarantined,\n chainHeight: witnessed.chainHeight,\n chainHead: witnessed.chainHead,\n lastWitness: witnessed.lastWitness,\n results: witnessed.results\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"authority_ledger_witness_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.authorityLedgerReplay) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const replayUrl = new URL(String(req.url || \"\"), \"http://localhost\");\n const epochId = String(replayUrl.searchParams.get(\"epochId\") || \"\").trim() || undefined;\n const snapshot = getAuthorityLedgerWitnessSnapshotV1(auth.uid, epochId);\n const replay = replayAuthorityLedgerWitnessChainV1(auth.uid, epochId);\n sendJson(res, 200, {\n ok: true,\n snapshot,\n replay,\n schema: \"castle.rhizoh.authority_ledger_witness.v1.replay_surface\"\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"authority_ledger_replay_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.authorityEpochMerge) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`authority_epoch_merge:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const merged = assimilateAuthorityEpochMergeV1(auth.uid, body);\n if (!merged.ok) {\n sendJson(res, 400, merged);\n return;\n }\n sendJson(res, 200, {\n ok: true,\n mergeEvent: merged.mergeEvent,\n recentMerges: merged.recentMerges\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"authority_epoch_merge_failed\") });\n }\n return;\n }\n\n sendJson(res, 404, { ok: false, error: \"not_found\" });\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\nspiralWssForGenesis = wss;\nconst orchestrator = createOrchestrator();\nconst clientStats = new Map();\nlet broadcasterClientId = null;\nconst spiralState = {\n rooms: [{ id: \"room-default\", name: \"Spiral Default Arena\" }],\n activeByClient: new Map(),\n characters: new Map() // id -> {id,name,role,roomId,source}\n};\n\n/** Castle social rooms — keyed by `castleRoomKey` from {@link WS_MESSAGE.CASTLE_SOCIAL_PULSE}. */\nconst castleSocialRoomState = new Map();\nconst CASTLE_SOCIAL_MEMBER_TTL_MS = 90_000;\nconst CASTLE_SOCIAL_MAX_PULSE_BYTES = 8192;\nconst RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES = 5 * 1024 * 1024;\nconst RHIZOH_VOICE_LIVE_MAX_CHUNKS = 96;\n\n/**\n * Voice Engine v3 live media lane.\n * Client streams MediaRecorder chunks over the existing gateway WS; STOP reuses the\n * gateway ASR runner so orchestration remains the single authority downstream.\n */\nconst rhizohVoiceLiveSessions = new WeakMap();\n\nfunction appendRhizohVoiceLiveEventV0(session, type, payloadRefSeed = \"\") {\n if (!session) return null;\n session.eventSeq = (Number(session.eventSeq) || 0) + 1;\n const event = createRhizohImmutableEventV0({\n type,\n sessionId: session.sessionId,\n traceId: session.traceId,\n actorSeed: session.sessionId || session.traceId,\n eventSeq: session.eventSeq,\n payloadRef: createRhizohPayloadRefV0(payloadRefSeed || `${session.sessionId}:${type}:${session.eventSeq}`)\n });\n session.events = [...(session.events || []), event].slice(-64);\n return event;\n}\n\nfunction sendRhizohVoiceLiveEnvelopeV0(socket, type, payload) {\n try {\n if (socket?.readyState === 1) socket.send(JSON.stringify(createEnvelope(type, payload)));\n } catch {\n /* noop */\n }\n}\n\nfunction decodeRhizohVoiceLiveChunkV0(payload = {}) {\n const b64 = String(payload.audioBase64 || \"\").trim();\n if (!b64) return { ok: false, error: \"audio_base64_required\" };\n try {\n const buf = Buffer.from(b64, \"base64\");\n if (!buf.length) return { ok: false, error: \"audio_decode_empty\" };\n return { ok: true, buffer: buf };\n } catch {\n return { ok: false, error: \"audio_base64_invalid\" };\n }\n}\n\nfunction handleRhizohVoiceLiveStartV0(socket, payload = {}) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const sessionId = String(p.sessionId || `live_${Date.now().toString(36)}`).slice(0, 96);\n rhizohVoiceLiveSessions.set(socket, {\n sessionId,\n traceId: String(p.traceId || \"\").slice(0, 128),\n mimeType: String(p.mimeType || \"audio/webm\").slice(0, 80),\n languageCode: String(p.languageCode || \"tr-TR\").slice(0, 32),\n path: String(p.path || \"both\").slice(0, 24),\n chunks: [],\n chunkIndexes: [],\n totalBytes: 0,\n nextChunkIndex: 1,\n eventSeq: 0,\n events: [],\n startedAtMs: Date.now()\n });\n appendRhizohVoiceLiveEventV0(rhizohVoiceLiveSessions.get(socket), \"VOICE_LIVE_SESSION_STARTED\", `${sessionId}:live_session`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_START, {\n ok: true,\n sessionId,\n schema: \"castle.rhizoh.voice_live.gateway_lane.v0\"\n });\n}\n\nfunction handleRhizohVoiceLiveChunkV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const sequence = validateRhizohVoiceLiveChunkPayloadV0(payload, {\n expectedChunkIndex: session.nextChunkIndex\n });\n if (!sequence.ok) {\n rhizohVoiceLiveSessions.delete(socket);\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:chunk_reject`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: sequence.error,\n expectedChunkIndex: sequence.expectedChunkIndex,\n chunkIndex: sequence.chunkIndex\n });\n return;\n }\n const decoded = decodeRhizohVoiceLiveChunkV0(payload);\n if (!decoded.ok) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: decoded.error\n });\n return;\n }\n if (\n session.chunks.length >= RHIZOH_VOICE_LIVE_MAX_CHUNKS ||\n session.totalBytes + decoded.buffer.length > RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES\n ) {\n rhizohVoiceLiveSessions.delete(socket);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_audio_too_large\"\n });\n return;\n }\n session.chunks.push(decoded.buffer);\n session.chunkIndexes.push(sequence.chunkIndex);\n session.totalBytes += decoded.buffer.length;\n session.nextChunkIndex += 1;\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_CHUNK_ACCEPTED\", `${session.sessionId}:chunk:${sequence.chunkIndex}`);\n}\n\nfunction handleRhizohVoiceLiveStopV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n rhizohVoiceLiveSessions.delete(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const lastChunkIndex = Number(payload?.lastChunkIndex);\n if (Number.isInteger(lastChunkIndex) && lastChunkIndex !== session.chunks.length) {\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:stop_mismatch`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_chunk_count_mismatch\",\n expectedChunkCount: lastChunkIndex,\n receivedChunkCount: session.chunks.length\n });\n return;\n }\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_STOP_RECEIVED\", `${session.sessionId}:stop:${session.chunks.length}`);\n const audio = Buffer.concat(session.chunks);\n if (!audio.length) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"audio_empty\"\n });\n return;\n }\n runRhizohVoiceTranscribeV3({\n path: String(payload?.path || session.path || \"both\"),\n audioBase64: audio.toString(\"base64\"),\n mimeType: String(payload?.mimeType || session.mimeType || \"audio/webm\"),\n languageCode: String(payload?.languageCode || session.languageCode || \"tr-TR\"),\n traceId: String(payload?.traceId || session.traceId || \"\"),\n sessionId: session.sessionId,\n transport: \"gateway_ws_live\"\n })\n .then((result) => {\n appendRhizohVoiceLiveEventV0(session, result?.ok ? \"VOICE_TRANSCRIBE_FINAL\" : \"VOICE_TRANSCRIBE_FAILED\", `${session.sessionId}:final`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_FINAL, {\n ...result,\n sessionId: session.sessionId,\n transportPath: \"gateway_ws_live\",\n bytes: audio.length,\n chunkCount: session.chunks.length,\n chunkSequence: Object.freeze({\n ok: true,\n firstChunkIndex: session.chunkIndexes[0] || null,\n lastChunkIndex: session.chunkIndexes[session.chunkIndexes.length - 1] || null,\n receivedChunkCount: session.chunks.length\n }),\n eventTrace: Object.freeze((session.events || []).slice(-16)),\n recordedMs: Math.max(0, Date.now() - session.startedAtMs)\n });\n })\n .catch((e) => {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_transcribe_failed\",\n detail: String(e?.message || e)\n });\n });\n}\n\nfunction removeCastleSocialClientFromAllRooms(clientId) {\n const cid = String(clientId || \"\");\n if (!cid) return;\n for (const room of castleSocialRoomState.values()) {\n room.members.delete(cid);\n }\n}\n\nfunction buildCastleSocialRoomPayload(roomKey) {\n const room = castleSocialRoomState.get(roomKey);\n if (!room) return null;\n const now = Date.now();\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const roster = [...room.members.values()].map((m) => ({\n userId: m.userId,\n lastMs: m.lastMs,\n ...m.sp\n }));\n const peerFeeds =\n room.walPeers && room.walPeers.size\n ? [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }))\n : [];\n return { castleRoomKey: roomKey, seq: room.seq, roster, peerFeeds, ts: now };\n}\n\nconst CASTLE_WAL_PEER_MAX_FEED_BYTES = 48 * 1024;\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {{ ok: boolean, code?: string, reason?: string }} gate\n */\nfunction rejectWalPeerIngressV0(socket, gate) {\n const code = gate.code || \"wal_peer_rejected\";\n recordWalPeerRejectV0(code);\n recordSubstrateOtelEventV0(\"wal_peer_reject\", { code, reason: String(gate.reason || \"\").slice(0, 120) });\n socket.send(\n JSON.stringify(\n createEnvelope(WS_MESSAGE.ERROR, {\n error: gate.reason || \"WAL peer ingress rejected.\",\n code\n })\n )\n );\n}\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {Record<string, unknown> | null | undefined} walPeerFeed\n * @returns {boolean}\n */\nfunction gateWalPeerIngressV0(socket, walPeerFeed) {\n const authGate = assertWalPeerSocketAuthorityV0(socket);\n if (!authGate.ok) {\n rejectWalPeerIngressV0(socket, authGate);\n return false;\n }\n const sigGate = validateWalPeerFeedSignatureV0(\n walPeerFeed && typeof walPeerFeed === \"object\" ? walPeerFeed : {}\n );\n if (!sigGate.ok) {\n rejectWalPeerIngressV0(socket, sigGate);\n return false;\n }\n return true;\n}\n\nfunction handleCastleWalPeerFeed(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_WAL_PEER_MAX_FEED_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_WAL_PEER_FEED too large.\" })));\n return;\n }\n const walPeerFeed = p.walPeerFeed && typeof p.walPeerFeed === \"object\" ? p.walPeerFeed : {};\n if (!gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const castleRoomKey = String(p.castleRoomKey || \"wal:convergence\").slice(0, 64);\n const castleId = String(p.castleId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId,\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: room.members.get(socket.clientId)?.sp ?? {}\n });\n const peerFeeds = [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }));\n const body = {\n castleRoomKey,\n seq: room.seq,\n peerFeeds,\n ts: now\n };\n const env = createEnvelope(WS_MESSAGE.CASTLE_WAL_PEER_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys(), ...room.walPeers.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction handleCastleSocialPulse(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_SOCIAL_MAX_PULSE_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_SOCIAL_PULSE too large.\" })));\n return;\n }\n const castleRoomKey = String(p.castleRoomKey || \"default\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n const sp = p.socialPulse && typeof p.socialPulse === \"object\" ? p.socialPulse : {};\n const walPeerFeed =\n p.walPeerFeed && typeof p.walPeerFeed === \"object\"\n ? p.walPeerFeed\n : sp.walPeerFeed && typeof sp.walPeerFeed === \"object\"\n ? sp.walPeerFeed\n : null;\n if (walPeerFeed && !gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const spNext = walPeerFeed ? { ...sp, walPeerFeed } : { ...sp };\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: spNext\n });\n if (walPeerFeed) {\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId: String(p.castleId || userId).slice(0, 64),\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n }\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const body = buildCastleSocialRoomPayload(castleRoomKey);\n if (!body) return;\n const env = createEnvelope(WS_MESSAGE.CASTLE_SOCIAL_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcast(msgObj) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcastState() {\n broadcast(\n createEnvelope(WS_MESSAGE.BROADCAST_STATE, {\n broadcasterClientId,\n viewers: [...wss.clients].filter((c) => c.readyState === 1).length\n })\n );\n}\n\nfunction currentSpiralStatePayload() {\n return {\n rooms: spiralState.rooms,\n characters: [...spiralState.characters.values()]\n };\n}\n\nfunction broadcastSpiralState() {\n broadcast(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload()));\n}\n\nfunction ensureRoom(roomId, fallbackName = null) {\n if (!roomId) return spiralState.rooms[0].id;\n const exists = spiralState.rooms.find((r) => r.id === roomId);\n if (exists) return exists.id;\n const created = { id: roomId, name: fallbackName || `Spiral-${roomId.slice(-4)}` };\n spiralState.rooms.push(created);\n return created.id;\n}\n\nfunction syncSpiralCharactersWithWorld(snapshot) {\n const agents = snapshot?.agents || [];\n for (const a of agents) {\n if (!spiralState.characters.has(a.id)) {\n spiralState.characters.set(a.id, {\n id: a.id,\n name: a.id,\n role: \"agent\",\n roomId: spiralState.rooms[0].id,\n source: \"world\"\n });\n }\n }\n}\n\nfunction broadcastExcept(msgObj, exceptSocket) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client !== exceptSocket && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction validateInputFrame(payload, socket) {\n const stats = clientStats.get(socket.clientId) || { lastFrame: 0, windowStart: Date.now(), msgCount: 0 };\n const now = Date.now();\n if (now - stats.windowStart > 5000) {\n stats.windowStart = now;\n stats.msgCount = 0;\n }\n stats.msgCount += 1;\n clientStats.set(socket.clientId, stats);\n if (stats.msgCount > 120) return \"Rate limit exceeded.\";\n\n if (!payload || !Number.isInteger(payload.frame)) return \"frame must be integer.\";\n if (payload.frame <= stats.lastFrame) return \"frame must be monotonic.\";\n const commands = Array.isArray(payload.commands) ? payload.commands : [];\n if (commands.length > 8) return \"Too many commands in one frame.\";\n for (const cmd of commands) {\n if (!cmd || typeof cmd.command !== \"string\") return \"Invalid command format.\";\n if (\n cmd.command !== \"SPAWN_AGENT\" &&\n cmd.command !== \"LIST_CASTLES\" &&\n cmd.command !== \"SPAWN_ENTITY\" &&\n cmd.command !== \"CREATE_CASTLE\"\n ) return \"Unsupported command.\";\n }\n stats.lastFrame = payload.frame;\n clientStats.set(socket.clientId, stats);\n return null;\n}\n\nfunction validateSignalPayload(payload) {\n if (!payload || typeof payload !== \"object\") return \"Invalid signaling payload.\";\n const signalType = payload.signalType;\n if (Object.values(CASTLE_NETWORK_SIGNAL_V0).includes(signalType)) {\n return null;\n }\n if (![\"OFFER\", \"ANSWER\", \"ICE\"].includes(signalType)) return \"Invalid signal type.\";\n if (typeof payload.to !== \"string\" || payload.to.length < 3) return \"Invalid signal destination.\";\n if (signalType !== \"ICE\" && JSON.stringify(payload.sdp || {}).length > 16 * 1024) return \"SDP too large.\";\n if (signalType === \"ICE\" && JSON.stringify(payload.candidate || {}).length > 6 * 1024) return \"ICE too large.\";\n return null;\n}\n\nfunction sanitizeRawMessage(raw) {\n if (!raw) return null;\n const text = raw.toString();\n if (Buffer.byteLength(text, \"utf8\") > MAX_MESSAGE_BYTES) return null;\n return text;\n}\n\nwss.on(\"connection\", async (socket, req) => {\n const origin = req?.headers?.origin || \"\";\n if (!httpCors.isWebSocketOriginAllowed(origin)) {\n socket.close(1008, \"Origin not allowed\");\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const path = req?.url || \"\";\n const query = new URL(`ws://localhost${path}`).searchParams;\n const token = query.get(\"token\");\n if (!token || token !== REQUIRED_GATEWAY_TOKEN) {\n socket.close(1008, \"Invalid gateway token\");\n return;\n }\n }\n if (REQUIRE_AUTH) {\n const result = await verifyClientToken(req);\n if (!result.ok) {\n const isLocal = origin.includes(\"localhost\") || origin.includes(\"127.0.0.1\") || !origin;\n if (!(ALLOW_DEV_ANON && isLocal)) {\n socket.close(1008, result.reason);\n return;\n }\n socket.auth = { ok: true, kind: \"dev-anon\", user: { uid: \"dev-anon\" } };\n } else {\n socket.auth = result;\n }\n }\n\n socket.clientId = `c-${Math.random().toString(36).slice(2, 8)}`;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.HELLO, { message: \"Castle Gateway online.\", clientId: socket.clientId })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.WORLD_SNAPSHOT, orchestrator.worldSnapshot())));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.BROADCAST_STATE, { broadcasterClientId, viewers: [...wss.clients].filter((c) => c.readyState === 1).length })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload())));\n broadcastExcept(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }), socket);\n\n socket.on(\"message\", (raw) => {\n const safeRaw = sanitizeRawMessage(raw);\n if (!safeRaw) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Message too large or invalid.\" })));\n return;\n }\n const parsed = safeJsonParse(safeRaw);\n if (!parsed?.type) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid message.\" })));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND_TEXT) {\n const canonical = parseTextToCommand(parsed.payload?.text);\n if (!canonical) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Unknown command text.\" })));\n return;\n }\n const result = orchestrator.applyCommand(canonical);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.MATCH_MOVE) {\n handleMatchMoveAuthorityV0(socket, parsed, wss);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND) {\n const result = orchestrator.applyCommand(parsed.payload);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n }\n\n if (parsed.type === WS_MESSAGE.INPUT_FRAME) {\n const err = validateInputFrame(parsed.payload, socket);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const frame = parsed.payload.frame;\n const commands = Array.isArray(parsed.payload?.commands) ? parsed.payload.commands : [];\n orchestrator.queueInputFrame(socket.clientId, frame, commands);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, queued: true, frame })));\n }\n\n if (parsed.type === WS_MESSAGE.SIGNAL) {\n const payload = parsed.payload || {};\n if (Object.values(CASTLE_NETWORK_SIGNAL_V0).includes(payload.signalType)) {\n const netErr = validateCastleNetworkSignalPayloadV0(socket, payload);\n if (netErr) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: netErr })));\n return;\n }\n handleCastleNetworkSignalV0(socket, payload, wss);\n return;\n }\n const err = validateSignalPayload(payload);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const signalType = payload.signalType;\n const to = payload.to;\n for (const client of wss.clients) {\n if (client.clientId === to && client.readyState === 1) {\n client.send(JSON.stringify(createEnvelope(WS_MESSAGE.SIGNAL, { ...payload, from: socket.clientId })));\n break;\n }\n }\n }\n\n if (parsed.type === WS_MESSAGE.DRONE_TELEMETRY) {\n const payload = parsed.payload || {};\n const json = JSON.stringify(payload);\n if (Buffer.byteLength(json, \"utf8\") > 48 * 1024) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"DRONE_TELEMETRY payload too large.\" })));\n return;\n }\n broadcastExcept(createEnvelope(WS_MESSAGE.DRONE_TELEMETRY, { ...payload, relayFrom: socket.clientId }), socket);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_START) {\n handleRhizohVoiceLiveStartV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_CHUNK) {\n handleRhizohVoiceLiveChunkV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_STOP) {\n handleRhizohVoiceLiveStopV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_SOCIAL_PULSE) {\n handleCastleSocialPulse(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_WAL_PEER_FEED) {\n handleCastleWalPeerFeed(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.OPEN_DATA_QUERY) {\n queryOpenData(parsed.payload)\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: true, result })));\n })\n .catch((e) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: false, error: e.message })));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_QUERY) {\n runRhizohBrainV2({\n ...(parsed.payload || {}),\n uid: socket?.auth?.user?.uid || \"unknown\"\n })\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, result)));\n })\n .catch(async () => {\n const fallback = await runRhizohBrain(parsed.payload || {}).catch(() => ({\n ok: false,\n reply: \"Rhizoh temporary disturbance. Habitat remains stable.\"\n }));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, fallback)));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.BROADCAST_REGISTER) {\n const role = String(parsed.payload?.role || \"\");\n if (role === \"GENESIS_BROADCAST_AGENT\") {\n broadcasterClientId = socket.clientId;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, registered: role })));\n broadcastState();\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.STUDIO_CUE) {\n // Only registered broadcaster can push global cues.\n if (!broadcasterClientId || socket.clientId !== broadcasterClientId) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Only broadcaster agent can emit STUDIO_CUE.\" })));\n return;\n }\n const cue = parsed.payload || {};\n if (typeof cue !== \"object\") {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid STUDIO_CUE payload.\" })));\n return;\n }\n broadcast(createEnvelope(WS_MESSAGE.STUDIO_CUE, cue));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_JOIN_ROOM) {\n const requested = String(parsed.payload?.roomId || \"\").trim();\n const requestedName = String(parsed.payload?.roomName || \"\").trim();\n const roomId = ensureRoom(requested || `room-${Math.random().toString(36).slice(2, 7)}`, requestedName || null);\n spiralState.activeByClient.set(socket.clientId, roomId);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, roomId })));\n broadcastSpiralState();\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_CREATE_CHARACTER) {\n const name = String(parsed.payload?.name || \"\").trim().slice(0, 32) || `AI-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;\n const role = String(parsed.payload?.role || \"agent\").trim().slice(0, 24) || \"agent\";\n const roomId = ensureRoom(\n String(parsed.payload?.roomId || spiralState.activeByClient.get(socket.clientId) || spiralState.rooms[0].id),\n null\n );\n\n // Character creation always maps to an authoritative world agent.\n const result = orchestrator.applyCommand({\n command: COMMAND.SPAWN_AGENT,\n params: { targetCastleId: \"castle-01\" }\n });\n if (!result?.ok || !result?.data?.id) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Character creation failed.\" })));\n return;\n }\n const id = result.data.id;\n spiralState.characters.set(id, { id, name, role, roomId, source: \"spawned\" });\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, characterId: id, roomId })));\n broadcastSpiralState();\n return;\n }\n });\n\n socket.on(\"close\", () => {\n if (socket.clientId === broadcasterClientId) broadcasterClientId = null;\n clientStats.delete(socket.clientId);\n rhizohVoiceLiveSessions.delete(socket);\n spiralState.activeByClient.delete(socket.clientId);\n removeCastleSocialClientFromAllRooms(socket.clientId);\n removeCastleNetworkClientV0(socket.clientId, wss);\n broadcast(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }));\n broadcastState();\n });\n});\n\nsetInterval(() => {\n void (async () => {\n try {\n const { frame, snapshot, delta, packedDelta } = orchestrator.tick(1 / 20);\n syncSpiralCharactersWithWorld(snapshot);\n broadcast(createEnvelope(WS_MESSAGE.NETWORK_TICK, { frame }));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_TICK, snapshot));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA, delta));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA_PACKED, packedDelta));\n broadcastSpiralState();\n await runAcademyEventTick();\n const executed = await tickAutomations(Date.now());\n for (const a of executed) {\n await addTranscript(a.uid, {\n source: \"planner\",\n eventType: \"automation-executed\",\n text: `Automation executed: ${a.title} (${a.action})`,\n roomId: \"studio-main\",\n meta: { automationId: a.id, action: a.action, deviceId: a.deviceId || \"\" }\n });\n }\n } catch (e) {\n console.error(\"[GATEWAY] world tick / automations loop error:\", String(e?.message || e));\n }\n })();\n}, 50);\n\n(async () => {\n await rhizohRuntime.telemetry.initOpenTelemetry();\n if (String(process.env.CASTLE_GENESIS_DISK_PERSIST ?? \"\").trim() === \"\") {\n process.env.CASTLE_GENESIS_DISK_PERSIST = \"1\";\n }\n const genesisDiskBoot = await hydrateGenesisContinuityPersistenceBootV0();\n if (!genesisDiskBoot.ok) {\n console.warn(\"[GATEWAY] genesis disk hydrate failed; continuing with in-memory genesis continuity\");\n }\n httpServer.listen(PORT, () => {\n startGenesisCanonicalClock();\n installGenesisCheckpointSurfaceGetter(buildGenesisRuntimeSurfacePayloadLive);\n setGenesisContinuityAfterPublishHook((seq) => {\n noteGenesisCheckpointSeqCommitted(seq);\n noteGenesisContinuitySeqAcceptedV0(seq);\n });\n startGenesisContinuityInfraSampler(buildGenesisRuntimeSurfacePayloadLive, 2000);\n logProductionObservatorySurfaceGuardsV0();\n logGatewaySubstrateAuthorityGuardsV0();\n if (isLlmWorkerEnabledV0()) {\n try {\n const workerBoot = initLlmWorkerAtBootV0();\n console.log(\"[GATEWAY] llm worker boot:\", workerBoot);\n } catch (workerErr) {\n console.error(\"[GATEWAY] llm worker boot failed:\", workerErr);\n }\n }\n console.log(`[GATEWAY] ws/http://localhost:${PORT}`);\n console.log(\n `[GATEWAY] genesis continuity: GET ${rhizohRuntime.routes.genesisRuntime} | POST ${rhizohRuntime.routes.genesisIngress} | SSE ${rhizohRuntime.routes.genesisStream} | replay ${rhizohRuntime.routes.genesisReplay} | diff ${rhizohRuntime.routes.genesisReplayDiff} | equiv ${rhizohRuntime.routes.genesisReplayEquivalence} | analytics ${rhizohRuntime.routes.genesisReplayAnalytics} | evolution ${rhizohRuntime.routes.genesisReplayEvolution}`\n );\n if (String(process.env.CASTLE_GENESIS_OBSERVATORY_LIVE_V01 ?? \"\").trim() === \"1\") {\n console.log(\"GENESIS OBSERVATORY LIVE v0.1 — READ ONLY PHASE STARTED\");\n }\n });\n})().catch((e) => {\n console.error(\"[GATEWAY] fatal boot (before or during listen):\", e);\n process.exit(1);\n});\n"}, "3185ee5e9943eb596be2bf0d837ff4b4f21c8c83": {"t": 1785754126.422052, "n": 176799, "h": "908372839a0968fc", "c": "import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport dotenv from \"dotenv\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconsole.log(\"🔥 [GENESIS_BOOT] entry file:\", __filename);\nconsole.log(\"🔥 [GENESIS_BOOT] node env:\", process.env.NODE_ENV);\nconsole.log(\"🔥 [GENESIS_BOOT] pid:\", process.pid);\nconsole.log(\"🧬 [GENESIS_BOOT] cwd:\", process.cwd());\nconsole.log(\"🧬 [GENESIS_BOOT] import.meta.url:\", import.meta.url);\ndotenv.config({ path: path.join(__dirname, \"..\", \".env\") });\ndotenv.config({ path: path.join(__dirname, \"..\", \".env.local\"), override: true });\n\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport { createServer } from \"node:http\";\nimport { createOrchestrator } from \"@castle/orchestrator\";\nimport { parseTextToCommand } from \"@castle/command-dsl\";\nimport {\n WS_MESSAGE,\n COMMAND,\n createEnvelope,\n createRhizohImmutableEventV0,\n createRhizohPayloadRefV0,\n safeJsonParse,\n validateRhizohVoiceLiveChunkPayloadV0\n} from \"@castle/protocol\";\nimport { queryOpenData } from \"./openData.js\";\nimport { verifyClientToken } from \"./auth.js\";\nimport { runRhizohBrain } from \"./rhizohBrain.js\";\nimport { queryRhizohLlm } from \"./rhizohLlmGateway.js\";\nimport { rhizohGatewayTurn } from \"./rhizohGatewayTurn.js\";\nimport {\n enqueueLlmWorkerTaskAsyncV0,\n getLatestLlmWorkerTaskIdV0,\n getLlmWorkerDebugSnapshotV0,\n getLlmWorkerTaskSnapshotV0,\n initLlmWorkerAtBootV0,\n isLlmWorkerAsyncEnabledV0,\n isLlmWorkerEnabledV0,\n runLlmWorkerTaskV0\n} from \"./llmWorkerRuntime.js\";\nimport {\n RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n runRhizohVoiceTranscribeV3,\n rhizohVoiceTranscribeEnvV3\n} from \"./rhizohVoiceTranscribeV3.js\";\nimport { createHttpCorsPolicy, normalizeHttpOrigin, parseAllowedOriginsFromEnv } from \"./httpCorsPolicyV1.js\";\nimport {\n applyLanguagePropagationToLlmPayloadV1,\n applyLanguagePropagationToVoiceBodyV1,\n buildRhizohLanguagePropagationEchoV1,\n parseRhizohLanguagePropagationV1\n} from \"./rhizohLanguagePropagationV1.js\";\nimport { trimRhizohLlmGatewayPayloadV1 } from \"./rhizohLlmPayloadTrimV1.js\";\nimport {\n meshAppendDelta,\n meshContinuityAggregate,\n meshJoin,\n meshLeave,\n meshReplayFromNodeId,\n meshReplayFromSeq,\n meshSnapshot,\n meshSubscribeSse\n} from \"./presenceRoomMesh.js\";\nimport { runRhizohBrainV2 } from \"./rhizohBrainV2.js\";\nimport {\n listConnections,\n createConnection,\n updateConnection,\n deleteConnection,\n setDefaultConnection,\n resolveConnection\n} from \"./llmConnectionsStore.js\";\nimport { getCloudSyncSnapshotV0, mergeCloudSyncSnapshotV0 } from \"./castleCloudSyncStore.js\";\nimport {\n CASTLE_NETWORK_SIGNAL_V0,\n handleCastleNetworkSignalV0,\n listCastleNetworkPresenceV0,\n removeCastleNetworkClientV0,\n validateCastleNetworkSignalPayloadV0\n} from \"./castleNetworkRelayV0.js\";\nimport { checkHttpRateLimit, getHttpClientIp } from \"./castleHttpRateLimit.js\";\nimport {\n submitAbuseReportV0,\n listModerationQueueV0,\n softBlockUserV0,\n removeSoftBlockV0,\n listSoftBlockedUsersV0\n} from \"./ops/moderationMvpV0.js\";\nimport { listRecentAgentSnapshotsV0 } from \"./ops/agentObservabilityV0.js\";\nimport { readAgentContainmentConfigV0 } from \"./ops/agentContainmentV0.js\";\nimport { readCostContainmentConfigV0, getCostContainmentStatsV0 } from \"./ops/costContainmentV0.js\";\nimport { getGlobalCostLedgerSnapshotV0, getGclBackendStatusV0 } from \"./ops/globalCostLedgerV0.js\";\nimport { evaluateEconomicStrategyL3V0 } from \"./ops/economicStrategyEngineL3V0.js\";\nimport { getPhasedRolloutStatsV0, readPhasedRolloutTierV0 } from \"./ops/phasedRolloutV0.js\";\nimport { buildUnifiedStateNarrativeV0 } from \"./ops/unifiedStateNarrativeV0.js\";\nimport { resolveHardeningTenantIdV0 } from \"./ops/narrativeTenantIsolationV0.js\";\nimport {\n classifyStressResponseV0,\n verifyStressTaxonomyCoverageV0,\n verifyStressConflictResolutionV0\n} from \"./ops/stressResponseTaxonomyV0.js\";\nimport { verifyResolutionStabilityEnvelopeV0 } from \"./ops/resolutionStabilityEnvelopeV0.js\";\nimport { logLlmAccess } from \"./castleLlmAudit.js\";\nimport { appendMemory, listMemories, getMemoryContext, getPersonaGoalMemory, setPersonaGoalMemory, autoCompactMemories } from \"./memoryStore.js\";\nimport { getFirebasePersistence } from \"./firebasePersistence.js\";\nimport { registerAgentIdentity, listAgentIdentities, getAgentIdentity, updateAgentIdentity } from \"./agentIdentityStore.js\";\nimport { queueAcademyEvent, listAcademyEvents, resolveAcademyEvent, runAcademyEventTick } from \"./academyEventEngine.js\";\nimport { enqueuePdfJob, getPdfJob } from \"./pdfPipeline.js\";\nimport {\n createPublishSession,\n listPublishSessions,\n updatePublishSession,\n addTranscript,\n listTranscripts,\n searchMetadata,\n registerDevice,\n listDevices,\n updateDevice,\n appendTelemetry,\n listTelemetry,\n createAutomation,\n listAutomations,\n tickAutomations,\n enqueueCommand,\n listCommands,\n updateCommandStatus,\n registerSocialChannel,\n listSocialChannels\n} from \"./studioOpsStore.js\";\nimport { metrics as infraMetrics } from \"./infra/metrics.js\";\nimport { renderRhizohPrometheusMetrics, rhizohEnterpriseMetrics } from \"./infra/rhizohEnterpriseMetrics.js\";\nimport { scoreHealth } from \"./infra/healthScore.js\";\nimport { getTraceById, listTracesBySession } from \"./infra/traceRegistry.js\";\nimport { canonicalEpistemicSealString, hashAndSignEpistemicSeal, EPISTEMIC_SEAL_SCHEMA } from \"./epistemicSeal.js\";\nimport { persistEpistemicLedgerBatch } from \"./epistemicLedgerStore.js\";\nimport { persistEpistemicForecastBatch } from \"./epistemicForecastStore.js\";\nimport {\n persistAuthorityLedgerWitnessBatchV1,\n getAuthorityLedgerWitnessSnapshotV1,\n replayAuthorityLedgerWitnessChainV1\n} from \"./authorityLedgerWitnessStoreV1.js\";\nimport { assimilateAuthorityEpochMergeV1 } from \"./authorityEpochMergeStoreV1.js\";\nimport { buildRhizohExternalGroundTruthPayload } from \"./rhizohExternalGroundTruthGateway.js\";\nimport { ingestRhizohExternalLossBatchHttp } from \"./rhizohExternalLossIngestGateway.js\";\nimport {\n ingestRhizohProductOutcomeHttp,\n listRhizohProductOutcomeAggregates,\n verifyRhizohOutcomeHmac,\n verifyRhizohOutcomeSourceToken\n} from \"./rhizohProductOutcomeIngestGateway.js\";\nimport { initRhizoh } from \"./rhizohProductionBootstrap.js\";\nimport { handleAcademicObservatoryExportGetV0 } from \"./rhizoh/academicObservatoryHttpV0.js\";\nimport { handleCouncilAnomalyReasoningPostV0 } from \"./council/rhizohEpistemicCouncilHttpV0.js\";\nimport {\n handleLiveSportsBundleGetV0,\n handleLiveNewsHeadlinesGetV0,\n handleLiveWorldFeedGetV0\n} from \"./rhizoh/liveWorldFeedHttpV0.js\";\nimport {\n buildGenesisRuntimeSurfacePayload,\n recordGenesisEpistemicLedgerPersisted,\n recordGenesisEpistemicSealIssued,\n startGenesisCanonicalClock\n} from \"./genesisRuntimeSurfaceV0.js\";\nimport { registerGenesisSseClient, getGenesisContinuitySeq, setGenesisContinuityAfterPublishHook } from \"./genesisContinuityStreamHubV0.js\";\nimport { computeGenesisReplayFingerprintV0 } from \"./genesisReplayFingerprintV0.js\";\nimport { buildGenesisGatewayCapabilitiesV0 } from \"./genesisGatewayCapabilitiesV0.js\";\nimport {\n installGenesisCheckpointSurfaceGetter,\n noteGenesisCheckpointSeqCommitted,\n getLatestGenesisSignedCheckpoint\n} from \"./genesisContinuityCheckpointV0.js\";\nimport { hydrateGenesisContinuityPersistenceBootV0 } from \"./genesisContinuityHydrateBootV0.js\";\nimport { startGenesisContinuityInfraSampler } from \"./genesisContinuityInfraSamplerV0.js\";\nimport {\n genesisCheckpointQueryBySeqV0,\n genesisCheckpointQueryRangeV0,\n genesisCheckpointQueryLineageV0\n} from \"./genesisCheckpointQueryV0.js\";\nimport {\n genesisContinuityEventArchiveEnabled,\n queryGenesisContinuityEventArchiveV0,\n GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION\n} from \"./genesisContinuityEventArchiveV0.js\";\nimport { resolveGenesisReplayRouterV1, GENESIS_REPLAY_ROUTER_SCHEMA } from \"./genesisReplayRouterV1.js\";\nimport {\n computeGenesisReplayTemporalDiffV1,\n GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA\n} from \"./genesisReplayTemporalDiffV1.js\";\nimport { compareGenesisReplayEquivalenceV1 } from \"./genesisReplayEquivalenceV1.js\";\nimport { computeGenesisReplayAnalyticsV1 } from \"./genesisReplayAnalyticsV1.js\";\nimport { computeGenesisReplayEvolutionV1 } from \"./genesisReplayEvolutionV1.js\";\nimport { handleStripeCheckoutCreate, handleStripeMembershipWebhook } from \"./stripeWebhookMembership.js\";\nimport { buildRealityHealthPayload } from \"./edgeRealityConsistencyV1.js\";\nimport {\n assertWalPeerSocketAuthorityV0,\n buildGatewaySubstrateAuthoritySnapshotV0,\n logGatewaySubstrateAuthorityGuardsV0,\n validateWalPeerFeedSignatureV0\n} from \"./gatewaySubstrateAuthorityV0.js\";\nimport {\n buildSubstrateOperationalSnapshotV0,\n recordWalPeerRejectV0,\n renderSubstratePrometheusMetricsV0\n} from \"./infra/substrateOperationalMetrics.js\";\nimport { ingestClientSubstrateHealthV0 } from \"./rhizohSubstrateHealthIngestV0.js\";\nimport { recordSubstrateOtelEventV0 } from \"./infra/opentelemetrySubstrateV0.js\";\nimport {\n GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n getGenesisWorldObservationIngressStatusV0,\n ingestGenesisWorldObservationV0\n} from \"./genesisContinuityClientIngressV0.js\";\nimport {\n getGenesisContinuitySeqAuditV0,\n noteGenesisContinuitySeqAcceptedV0\n} from \"./genesisContinuitySeqGapV0.js\";\n\n/** Render / Fly / Railway: `PORT` — yoksa `CASTLE_GATEWAY_PORT` — yoksa 8090. */\nconst PORT =\n Number(process.env.PORT) ||\n Number(process.env.CASTLE_GATEWAY_PORT) ||\n 8090;\n/** Spiral / world WebSocket server — set after `wss` init; used by genesis runtime surface. */\nlet spiralWssForGenesis = null;\nconst MAX_MESSAGE_BYTES = Number(process.env.CASTLE_MAX_MESSAGE_BYTES || 32 * 1024);\nconst REQUIRED_GATEWAY_TOKEN = process.env.CASTLE_GATEWAY_TOKEN || \"\";\nconst ALLOWED_ORIGINS = parseAllowedOriginsFromEnv().fromList;\nconst httpCors = createHttpCorsPolicy();\n\n/** @returns {string|null} `Access-Control-Allow-Origin`; null = browser CORS fail. */\nfunction accessControlAllowOriginValue(req) {\n return httpCors.accessControlAllowOriginValue(req);\n}\n\nfunction applyHttpCorsHeaders(req, res) {\n return httpCors.applyHttpCorsHeaders(req, res);\n}\nconst REQUIRE_AUTH = process.env.CASTLE_REQUIRE_AUTH === \"true\";\nconst ALLOW_DEV_ANON = process.env.CASTLE_ALLOW_DEV_ANON !== \"false\";\nconst ALLOW_DEV_HTTP_UID = process.env.CASTLE_ALLOW_DEV_HTTP_UID !== \"false\";\n/** Presence mesh: allow `X-Castle-Guest-Id` when Firebase auth absent (default on). Set `false` to require auth only. */\nconst ALLOW_MESH_GUEST = process.env.CASTLE_ALLOW_MESH_GUEST !== \"false\";\nconst SOCIAL_RETRY_MAX = Math.max(0, Math.min(5, Number(process.env.CASTLE_SOCIAL_RETRY_MAX || 2)));\nconst SOCIAL_RETRY_BASE_MS = Math.max(100, Number(process.env.CASTLE_SOCIAL_RETRY_BASE_MS || 700));\nconst TELEGRAM_BOT_TOKEN = process.env.CASTLE_TELEGRAM_BOT_TOKEN || \"\";\nconst WHATSAPP_TOKEN = process.env.CASTLE_WHATSAPP_TOKEN || \"\";\nconst WHATSAPP_PHONE_NUMBER_ID = process.env.CASTLE_WHATSAPP_PHONE_NUMBER_ID || \"\";\nconst RL_RHIZOH_LLM_PER_MIN = Math.max(5, Number(process.env.CASTLE_RL_RHIZOH_LLM_PER_MIN || 40));\nconst RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN || 30)\n);\nconst RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN || 120));\nconst RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN = Math.max(\n 10,\n Number(process.env.CASTLE_RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN || 60)\n);\nconst RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN = Math.max(8, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN || 48));\nconst RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN = Math.max(4, Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN || 24));\nconst RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN || 18)\n);\nconst RL_EPISTEMIC_SEAL_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_EPISTEMIC_SEAL_PER_MIN || 120));\nconst RL_EPISTEMIC_LOG_PER_MIN = Math.max(20, Number(process.env.CASTLE_RL_EPISTEMIC_LOG_PER_MIN || 240));\nconst EPISTEMIC_SEAL_SECRET = String(\n process.env.CASTLE_EPISTEMIC_SEAL_SECRET || process.env.CASTLE_GATEWAY_TOKEN || \"\"\n).trim();\nconst RL_LLM_CONN_TEST_PER_MIN = Math.max(2, Number(process.env.CASTLE_RL_LLM_CONNECTION_TEST_PER_MIN || 8));\n\n/** One-shot production warnings: CORS surface, auth toggle, dev bypass flags (observatory deploy hygiene). */\nfunction logProductionObservatorySurfaceGuardsV0() {\n if (String(process.env.NODE_ENV || \"\").toLowerCase() !== \"production\") return;\n const tag = \"[GATEWAY_PROD_GUARD]\";\n const primaryCors = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n const corsListEmpty = ALLOWED_ORIGINS.length === 0;\n if (corsListEmpty && !primaryCors) {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty and CASTLE_HTTP_CORS_ORIGIN empty — production still merges RHIZOH_BROWSER_ORIGINS_V1 defaults; set env for preview/staging hosts`\n );\n } else if (corsListEmpty && primaryCors && primaryCors !== \"*\") {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty — only CASTLE_HTTP_CORS_ORIGIN set; add web.app + firebaseapp.com (and previews) to CASTLE_ALLOWED_ORIGINS if clients use alternate hostnames`\n );\n }\n if (primaryCors && /localhost|127\\.0\\.0\\.1/i.test(primaryCors)) {\n console.warn(`${tag} CORS: CASTLE_HTTP_CORS_ORIGIN is localhost while NODE_ENV=production`);\n }\n if (REQUIRE_AUTH) {\n console.warn(\n `${tag} AUTH: CASTLE_REQUIRE_AUTH=true — clients must send a valid gateway credential (otherwise silent 401 on replay/SSE); EventSource cannot set custom headers unless proxied`\n );\n if (REQUIRED_GATEWAY_TOKEN.length < 16) {\n console.warn(`${tag} AUTH: CASTLE_GATEWAY_TOKEN missing or short while require-auth enabled`);\n }\n }\n if (ALLOW_DEV_ANON) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_ANON is not \"false\" — dev anonymous path may be live in production (set CASTLE_ALLOW_DEV_ANON=false)`\n );\n }\n if (ALLOW_DEV_HTTP_UID) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_HTTP_UID is not \"false\" — X-Castle-Dev-Uid bypass risk (set CASTLE_ALLOW_DEV_HTTP_UID=false)`\n );\n }\n}\n\nconst rhizohRuntime = initRhizoh();\n\n/** İstemci: llmKeySource veya keySource — env | user_connection | auto */\nfunction normalizeClientLlmKeySource(raw) {\n const s = String(raw ?? \"\").trim().toLowerCase();\n if (s === \"env\" || s === \"server\") return \"env\";\n if (s === \"user_connection\" || s === \"connection\" || s === \"user\") return \"user_connection\";\n if (s === \"auto\" || s === \"\") return \"auto\";\n return null;\n}\n\n/**\n * Server-side provider wins for env/auto unless user_connection — client bundle may still send provider=openai.\n * @param {\"env\"|\"user_connection\"|\"auto\"} keyMode\n * @param {{ provider?: string, model?: string, apiKey?: string } | null} conn\n * @param {{ provider?: string, model?: string }} payload\n */\nfunction resolveServerLlmProviderV0(keyMode, conn, payload) {\n const envProvider = String(process.env.CASTLE_LLM_PROVIDER || \"\").trim() || undefined;\n const envModel = String(process.env.CASTLE_LLM_MODEL || \"\").trim() || undefined;\n\n if (keyMode === \"user_connection\") {\n return {\n resolvedProvider: conn?.provider || payload?.provider,\n resolvedModel: conn?.model || payload?.model,\n connApiKey: conn?.apiKey || \"\"\n };\n }\n\n if (keyMode === \"env\") {\n return {\n resolvedProvider: envProvider ?? payload?.provider,\n resolvedModel: envModel ?? payload?.model,\n connApiKey: \"\"\n };\n }\n\n if (conn?.provider && conn?.apiKey) {\n return {\n resolvedProvider: conn.provider,\n resolvedModel: conn.model,\n connApiKey: conn.apiKey\n };\n }\n\n return {\n resolvedProvider: envProvider ?? payload?.provider,\n resolvedModel: envModel ?? payload?.model,\n connApiKey: \"\"\n };\n}\n\nfunction jsonReplacerSafeV0(_key, value) {\n if (typeof value === \"bigint\") return Number(value);\n return value;\n}\n\nfunction sendJson(res, statusCode, payload) {\n res.writeHead(statusCode, { \"Content-Type\": \"application/json; charset=utf-8\" });\n try {\n res.end(JSON.stringify(payload, jsonReplacerSafeV0));\n } catch (error) {\n res.end(\n JSON.stringify({\n ok: false,\n error: \"response_serialize_failed\",\n detail: String(error?.message || error)\n })\n );\n }\n}\n\nfunction logRhizohHealth(stage, detail = {}) {\n try {\n console.info(`[CASTLE_${String(stage || \"unknown\")}]`, detail && typeof detail === \"object\" ? detail : {});\n } catch {\n /* noop */\n }\n}\n\nfunction renderInfraMetricsProm() {\n const lines = [\n `castle_gateway_events_processed_total ${infraMetrics.eventsProcessed}`,\n `castle_gateway_duplicate_reject_total ${infraMetrics.duplicateRejects}`,\n `castle_gateway_enqueue_latency_ms ${infraMetrics.enqueueLatencyMs}`,\n `castle_gateway_queue_depth ${infraMetrics.queueDepth}`,\n `castle_gateway_queue_lag_seconds ${(Number(infraMetrics.queueLag || 0) / 1000).toFixed(3)}`,\n `castle_gateway_errors_total ${infraMetrics.errors}`\n ];\n const buckets = infraMetrics.enqueueLatencyBuckets || {};\n for (const k of Object.keys(buckets)) {\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"${k}\"} ${buckets[k]}`);\n }\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"+Inf\"} ${infraMetrics.eventsProcessed}`);\n if (process.env.CASTLE_SUBSTRATE_METRICS !== \"0\") {\n lines.push(renderSubstratePrometheusMetricsV0().trimEnd());\n }\n return lines.join(\"\\n\");\n}\n\nasync function fetchJsonWithTimeout(url, timeoutMs = 1200) {\n const ctl = new AbortController();\n const tid = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const res = await fetch(url, { signal: ctl.signal });\n if (!res.ok) return null;\n return await res.json();\n } catch {\n return null;\n } finally {\n clearTimeout(tid);\n }\n}\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction getHttpPathname(req) {\n let p = String(req.url || \"\").split(\"?\")[0];\n /* Reverse proxies / clients sometimes append `/` — avoid silent 404 on e.g. `/rhizoh/genesis/stream/`. */\n while (p.length > 1 && p.endsWith(\"/\")) {\n p = p.slice(0, -1);\n }\n return p;\n}\n\nfunction rhizohLlmEnvConfigured() {\n const p = String(process.env.CASTLE_LLM_PROVIDER || \"openai\").toLowerCase();\n const key =\n p === \"anthropic\"\n ? process.env.ANTHROPIC_API_KEY\n : p === \"gemini\"\n ? process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY\n : p === \"xai\"\n ? process.env.XAI_API_KEY\n : p === \"deepseek\"\n ? process.env.DEEPSEEK_API_KEY\n : p === \"mistral\"\n ? process.env.MISTRAL_API_KEY\n : p === \"openrouter\"\n ? process.env.OPENROUTER_API_KEY\n : process.env.OPENAI_API_KEY;\n return !!String(key || \"\").trim();\n}\n\n/**\n * @param {import(\"firebase-admin/firestore\").Firestore | null} db\n * @param {number} timeoutMs\n */\nasync function probeFirestoreReachable(db, timeoutMs = 2000) {\n if (!db) return { ok: false, reason: \"no_db\" };\n const timeout = sleep(timeoutMs).then(() => ({ ok: false, reason: \"timeout\" }));\n const work = db\n .collection(\"_castle_gateway_health\")\n .doc(\"ping\")\n .get()\n .then(() => ({ ok: true, reason: \"reachable\" }))\n .catch((e) => ({ ok: false, reason: String(e?.message || e || \"firestore_error\") }));\n return Promise.race([work, timeout]);\n}\n\nfunction stripHtml(text = \"\") {\n return String(text)\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \" \")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \" \")\n .replace(/<[^>]+>/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction haversineMeters(aLat, aLon, bLat, bLon) {\n const R = 6371000;\n const dLat = ((bLat - aLat) * Math.PI) / 180;\n const dLon = ((bLon - aLon) * Math.PI) / 180;\n const sa = Math.sin(dLat / 2);\n const sb = Math.sin(dLon / 2);\n const x = sa * sa + Math.cos((aLat * Math.PI) / 180) * Math.cos((bLat * Math.PI) / 180) * sb * sb;\n return 2 * R * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));\n}\n\nasync function checkPublishTargetHealth(protocol, target) {\n const p = String(protocol || \"\").toUpperCase();\n const t = String(target || \"\").trim();\n if (!t) return { ok: false, reason: \"target_missing\", shouldReconnect: false };\n if (p === \"RTMP\") {\n const ok = t.startsWith(\"rtmp://\") || t.startsWith(\"rtmps://\");\n return { ok, reason: ok ? \"rtmp_target_format_ok\" : \"invalid_rtmp_target\", shouldReconnect: !ok, reconnectInSec: 15 };\n }\n if (p === \"WHIP\" || p === \"SFU\") {\n try {\n const res = await fetch(t, { method: \"OPTIONS\" });\n const ok = res.status < 500;\n return { ok, reason: `http_${res.status}`, shouldReconnect: !ok, reconnectInSec: 8 };\n } catch {\n return { ok: false, reason: \"network_unreachable\", shouldReconnect: true, reconnectInSec: 8 };\n }\n }\n return { ok: true, reason: \"unchecked_protocol\", shouldReconnect: false };\n}\n\nasync function fetchTextFromUrl(url) {\n const res = await fetch(url, { method: \"GET\" });\n if (!res.ok) throw new Error(`fetch_${res.status}`);\n const ct = String(res.headers.get(\"content-type\") || \"\");\n if (ct.includes(\"application/pdf\")) throw new Error(\"pdf_binary_not_supported_directly\");\n const body = await res.text();\n return stripHtml(body).slice(0, 14000);\n}\n\nfunction buildTaskActionGraph(task = \"\", context = {}) {\n const t = String(task || \"\").toLowerCase();\n if (t.includes(\"dinner\") || t.includes(\"akşam yemeği\") || t.includes(\"aksam yemegi\")) {\n return {\n taskTitle: \"Dinner Prep Planner\",\n nodes: [\n { id: \"n1\", title: \"Kitchen preheat\", action: \"smart-oven.preheat\", params: { temperatureC: 190, mode: \"fan\" } },\n { id: \"n2\", title: \"Lighting comfort\", action: \"smart-light.set_scene\", params: { scene: \"warm_dinner\" } },\n { id: \"n3\", title: \"Fridge inventory check\", action: \"smart-fridge.inventory_scan\", params: {} },\n { id: \"n4\", title: \"Reminder notification\", action: \"smart-watch.notify\", params: { text: \"Dinner prep started.\" } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" },\n { from: \"n3\", to: \"n4\" }\n ]\n };\n }\n return {\n taskTitle: task || \"General Planner\",\n nodes: [\n { id: \"n1\", title: \"Safety check\", action: \"device.safety_check\", params: { context } },\n { id: \"n2\", title: \"Primary action\", action: \"device.execute_task\", params: { task } },\n { id: \"n3\", title: \"Report\", action: \"studio.timeline_note\", params: { task } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" }\n ]\n };\n}\n\nfunction buildEthicsPrograms() {\n return {\n policyVersion: \"v1\",\n tracks: [\n {\n id: \"elderly-care-assist\",\n audience: \"elderly\",\n title: \"Elderly Assistance Track\",\n principles: [\"human-in-the-loop\", \"consent-first\", \"low-friction alerts\"],\n modules: [\"medication-reminder\", \"fall-risk-check\", \"family-notify\", \"daily-companion-brief\"]\n },\n {\n id: \"kids-safe-learning\",\n audience: \"children\",\n title: \"Kids Safe Learning Track\",\n principles: [\"guardian-consent\", \"age-appropriate-language\", \"content-filtering\"],\n modules: [\"study-coach\", \"screen-time-balance\", \"safe-social-share\", \"emergency-contact\"]\n }\n ]\n };\n}\n\nasync function deliverViaTelegram(channel, text) {\n const chatId = String(channel.endpoint || \"\").trim();\n if (!TELEGRAM_BOT_TOKEN) throw new Error(\"telegram_token_missing\");\n if (!chatId) throw new Error(\"telegram_chat_id_missing\");\n const url = `https://api.telegram.org/bot${encodeURIComponent(TELEGRAM_BOT_TOKEN)}/sendMessage`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ chat_id: chatId, text })\n });\n if (!res.ok) throw new Error(`telegram_http_${res.status}`);\n return { provider: \"telegram\", status: res.status };\n}\n\nasync function deliverViaWhatsapp(channel, text) {\n const to = String(channel.endpoint || \"\").trim();\n if (!WHATSAPP_TOKEN) throw new Error(\"whatsapp_token_missing\");\n if (!WHATSAPP_PHONE_NUMBER_ID) throw new Error(\"whatsapp_phone_number_id_missing\");\n if (!to) throw new Error(\"whatsapp_to_missing\");\n const url = `https://graph.facebook.com/v22.0/${encodeURIComponent(WHATSAPP_PHONE_NUMBER_ID)}/messages`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${WHATSAPP_TOKEN}` },\n body: JSON.stringify({ messaging_product: \"whatsapp\", to, type: \"text\", text: { body: text } })\n });\n if (!res.ok) throw new Error(`whatsapp_http_${res.status}`);\n return { provider: \"whatsapp\", status: res.status };\n}\n\nasync function deliverViaWebhook(channel, text) {\n const endpoint = String(channel.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"webhook_endpoint_missing\");\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text, platform: channel.platform || \"webhook\", ts: Date.now() })\n });\n if (!res.ok) throw new Error(`webhook_http_${res.status}`);\n return { provider: \"webhook\", status: res.status };\n}\n\nasync function deliverSocialMessage(channel, text) {\n const platform = String(channel.platform || \"webhook\").toLowerCase();\n if (platform === \"telegram\") return deliverViaTelegram(channel, text);\n if (platform === \"whatsapp\") return deliverViaWhatsapp(channel, text);\n return deliverViaWebhook(channel, text);\n}\n\nasync function deliverSocialWithRetry(channel, text) {\n let attempt = 0;\n let lastError = null;\n while (attempt <= SOCIAL_RETRY_MAX) {\n try {\n const result = await deliverSocialMessage(channel, text);\n return { ok: true, attempt: attempt + 1, ...result };\n } catch (error) {\n lastError = error;\n if (attempt >= SOCIAL_RETRY_MAX) break;\n const jitter = Math.floor(Math.random() * 90);\n await sleep(SOCIAL_RETRY_BASE_MS * Math.pow(2, attempt) + jitter);\n }\n attempt += 1;\n }\n return { ok: false, attempt: attempt + 1, error: String(lastError?.message || \"delivery_failed\") };\n}\n\nasync function dispatchCommandToAdapter(device, command) {\n const adapter = String(command?.adapter || device?.adapter || \"websocket\").toLowerCase();\n const endpoint = String(device?.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"device_endpoint_missing\");\n if (adapter === \"mqtt\") {\n // MQTT delivery is proxied via a bridge endpoint (HTTP webhook style).\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n type: \"mqtt-command\",\n topic: `castle/devices/${device.id}/commands`,\n action: command.action,\n params: command.params || {}\n })\n });\n if (!res.ok) throw new Error(`mqtt_bridge_http_${res.status}`);\n return { ok: true, adapter, via: \"http-bridge\", status: res.status };\n }\n if (!endpoint.startsWith(\"ws://\") && !endpoint.startsWith(\"wss://\")) throw new Error(\"invalid_websocket_endpoint\");\n const sendResult = await new Promise((resolve, reject) => {\n const ws = new WebSocket(endpoint);\n const timer = setTimeout(() => {\n try {\n ws.close();\n } catch {\n /* noop */\n }\n reject(new Error(\"websocket_timeout\"));\n }, 3000);\n ws.on(\"open\", () => {\n ws.send(\n JSON.stringify({\n type: \"castle-command\",\n commandId: command.id,\n deviceId: command.deviceId,\n action: command.action,\n params: command.params || {}\n }),\n (err) => {\n clearTimeout(timer);\n ws.close();\n if (err) reject(new Error(\"websocket_send_failed\"));\n else resolve({ ok: true, adapter: \"websocket\", via: \"ws\" });\n }\n );\n });\n ws.on(\"error\", () => {\n clearTimeout(timer);\n reject(new Error(\"websocket_connect_failed\"));\n });\n });\n return sendResult;\n}\n\nfunction readHttpJson(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => {\n if (!buf) return resolve({});\n const payload = safeJsonParse(buf);\n if (!payload || typeof payload !== \"object\") return reject(new Error(\"invalid_json_payload\"));\n resolve(payload);\n });\n req.on(\"error\", reject);\n });\n}\n\n/** Ham gövde (HMAC doğrulaması için). */\nfunction readHttpBodyText(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => resolve(buf));\n req.on(\"error\", reject);\n });\n}\n\nfunction maskOpaqueId(value) {\n const t = String(value || \"\");\n if (!t) return \"(empty)\";\n if (t.length <= 8) return \"****\";\n return `${t.slice(0, 4)}…${t.slice(-2)}`;\n}\n\nfunction readGatewayHttpToken(req) {\n const x = String(req.headers?.[\"x-castle-gateway-token\"] || \"\").trim();\n if (x) return x;\n const auth = String(req.headers?.authorization || \"\");\n const m = /^Bearer\\s+(.+)$/i.exec(auth);\n return m ? String(m[1] || \"\").trim() : \"\";\n}\n\nasync function resolveHttpUser(req) {\n const authResult = await verifyClientToken(req);\n if (authResult?.ok && authResult.user?.uid) {\n return { ok: true, uid: String(authResult.user.uid), mode: authResult.kind || \"auth\" };\n }\n if (ALLOW_DEV_ANON && ALLOW_DEV_HTTP_UID) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) return { ok: true, uid: `dev-${devUid.slice(0, 64)}`, mode: \"dev-header\" };\n }\n return { ok: false, reason: authResult?.reason || \"auth_required\" };\n}\n\n/**\n * Epistemic ledger ingest — Firebase uid OR valid transport token + X-Castle-Dev-Uid.\n * Prod disables CASTLE_ALLOW_DEV_HTTP_UID; LLM may still work while telemetry 401'd without this.\n */\nasync function resolveEpistemicIngestActor(req) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok && tok === REQUIRED_GATEWAY_TOKEN) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) {\n return { ok: true, uid: `gw-telemetry-${devUid.slice(0, 64)}`, mode: \"gateway-token-dev-uid\" };\n }\n return { ok: false, reason: \"dev_uid_required\" };\n }\n if (tok) {\n return { ok: false, reason: \"gateway_token_mismatch\" };\n }\n }\n const auth = await resolveHttpUser(req);\n if (auth.ok) return auth;\n return { ok: false, reason: auth.reason || \"auth_required\" };\n}\n\n/** Mesh actor: Firebase uid, dev header, or `guest:<uuid>` via header / `guestId` query (SSE cannot set headers). */\nasync function resolveMeshActor(req) {\n const auth = await resolveHttpUser(req);\n if (auth.ok) return { ok: true, clientUid: auth.uid, mode: auth.mode };\n if (ALLOW_MESH_GUEST) {\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const gid = String(req.headers?.[\"x-castle-guest-id\"] || q.searchParams.get(\"guestId\") || \"\").trim();\n if (gid) {\n const normalized = gid.startsWith(\"guest:\") ? gid.slice(0, 180) : `guest:${gid.slice(0, 160)}`;\n return { ok: true, clientUid: normalized, mode: \"guest-header\" };\n }\n }\n return { ok: false, reason: \"mesh_auth_required\" };\n}\n\n/** Live genesis runtime JSON (GET /rhizoh/genesis/runtime + SSE infra sampler). */\nasync function buildGenesisRuntimeSurfacePayloadLive() {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n let spiralWsActive = 0;\n if (spiralWssForGenesis) {\n for (const c of spiralWssForGenesis.clients) {\n if (c.readyState === 1) spiralWsActive += 1;\n }\n }\n const base = buildGenesisRuntimeSurfacePayload({\n infraMetrics,\n scoredHealth: scored,\n rhizohEnterpriseMetrics,\n mesh: meshContinuityAggregate(),\n workerHealth: worker,\n port: PORT,\n spiralWebSocketClientsActive: spiralWsActive\n });\n return {\n ...base,\n replayFingerprint: computeGenesisReplayFingerprintV0(base),\n gatewayCapabilities: buildGenesisGatewayCapabilitiesV0(),\n genesisStream: {\n schema: \"castle.genesis.stream_cursor.v0\",\n lastAcceptedSeq: getGenesisContinuitySeq(),\n clientIngress: {\n schema: GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n contract: \"castle.world_observation.ingress_envelope.v1\",\n postPath: rhizohRuntime.routes.genesisIngress,\n closure: getGenesisWorldObservationIngressStatusV0()\n },\n seqContinuity: getGenesisContinuitySeqAuditV0(),\n eventArchive: genesisContinuityEventArchiveEnabled()\n ? {\n enabled: true,\n projection: GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION,\n queryPath: rhizohRuntime.routes.genesisContinuityEvents\n }\n : { enabled: false },\n replayRouter: {\n schema: GENESIS_REPLAY_ROUTER_SCHEMA,\n queryPath: rhizohRuntime.routes.genesisReplay,\n queryHint: \"from=&to= or range=from-to; optional type=&checkpoints=0\",\n temporalDiffPath: rhizohRuntime.routes.genesisReplayDiff,\n temporalDiffSchema: GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA,\n equivalencePath: rhizohRuntime.routes.genesisReplayEquivalence,\n analyticsPath: rhizohRuntime.routes.genesisReplayAnalytics,\n evolutionPath: rhizohRuntime.routes.genesisReplayEvolution\n }\n }\n };\n}\n\nconst httpServer = createServer(async (req, res) => {\n const corsAllow = applyHttpCorsHeaders(req, res);\n if (req.method === \"OPTIONS\") {\n if (!corsAllow && req.headers?.origin) {\n console.warn(\"[GATEWAY_CORS] preflight rejected for origin:\", req.headers.origin);\n }\n res.writeHead(corsAllow ? 204 : 403);\n res.end();\n return;\n }\n\n const pathname = getHttpPathname(req);\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/genesis/__ping\") {\n sendJson(res, 200, {\n ok: true,\n pid: process.pid,\n entry: __filename,\n time: Date.now(),\n genesis: true\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/epistemic/__probe\") {\n sendJson(res, 200, {\n ok: true,\n epistemicIngestActorV2: true,\n routes: {\n seal: rhizohRuntime.routes.epistemicSeal,\n logsBatch: rhizohRuntime.routes.epistemicLogsBatch,\n authorityLedgerBatch: rhizohRuntime.routes.authorityLedgerBatch,\n authorityLedgerReplay: rhizohRuntime.routes.authorityLedgerReplay,\n authorityEpochMerge: rhizohRuntime.routes.authorityEpochMerge\n },\n hasGatewayToken: Boolean(REQUIRED_GATEWAY_TOKEN),\n gatewayTokenLen: REQUIRED_GATEWAY_TOKEN ? REQUIRED_GATEWAY_TOKEN.length : 0\n });\n return;\n }\n\n if (\n req.method === \"GET\" &&\n (pathname === \"/rhizoh/live/world-feed\" ||\n pathname === \"/rhizoh/live/sports-bundle\" ||\n pathname === \"/rhizoh/live/news-headlines\")\n ) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_live_world_feed:${ip}`, RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const out =\n pathname === \"/rhizoh/live/sports-bundle\"\n ? await handleLiveSportsBundleGetV0()\n : pathname === \"/rhizoh/live/news-headlines\"\n ? await handleLiveNewsHeadlinesGetV0(req)\n : await handleLiveWorldFeedGetV0(req);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/webhooks/stripe\") {\n await handleStripeMembershipWebhook(req, res);\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/billing/stripe/checkout\") {\n await handleStripeCheckoutCreate(req, res, { readHttpJson, resolveHttpUser, sendJson });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/live\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, live: false, dns: true, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n sendJson(res, 200, { ok: true, live: true, dns: true, service: \"castle-gateway\", ts: Date.now() });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/cors\") {\n const reqOrigin = normalizeHttpOrigin(req.headers?.origin);\n const allowOrigin = accessControlAllowOriginValue(req);\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n requestOrigin: reqOrigin || null,\n allowed: Boolean(allowOrigin),\n allowOrigin: allowOrigin || null,\n allowSetSize: httpCors.allowSet.size\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/ready\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, ready: false, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n try {\n const persistence = getFirebasePersistence();\n const llmProbe = rhizohLlmEnvConfigured();\n sendJson(res, 200, {\n ok: true,\n ready: true,\n dns: true,\n persistence: persistence.mode,\n service: \"castle-gateway\",\n gateway: \"ok\",\n llm: llmProbe ? \"ok\" : \"degraded\",\n spine: llmProbe ? \"ok\" : \"partial\"\n });\n } catch (e) {\n sendJson(res, 503, { ok: false, ready: false, reason: String(e?.message || e), service: \"castle-gateway\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/reality\") {\n const payload = buildRealityHealthPayload();\n sendJson(res, payload.ok ? 200 : 503, payload);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/substrate\") {\n const authority = buildGatewaySubstrateAuthoritySnapshotV0();\n const operational = buildSubstrateOperationalSnapshotV0();\n const drift = buildRealityHealthPayload();\n const ok = Boolean(authority.productionAuthoritySatisfied) && drift.ok;\n sendJson(res, ok ? 200 : 503, {\n ok,\n service: \"castle-gateway\",\n ts: Date.now(),\n authority,\n operational,\n drift: { ok: drift.ok, errors: drift.drift?.errors, warnings: drift.drift?.warnings }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.councilAnomalyReasoning) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_council_anomaly:${ip}`, 30, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const body = await readHttpJson(req);\n const out = handleCouncilAnomalyReasoningPostV0(body);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/substrate/health\") {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const body = await readHttpJson(req);\n const uid = (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestClientSubstrateHealthV0(body, { uid, clientId: uid });\n if (!result.ok) {\n sendJson(res, 400, result);\n return;\n }\n recordSubstrateOtelEventV0(\"client_health_ingest\", { uid: String(uid).slice(0, 32) });\n sendJson(res, 200, result);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/deps\") {\n const t0 = Date.now();\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, {\n ok: false,\n dns: true,\n llm: false,\n firestore: false,\n phase: \"maintenance\",\n latencyMs: Date.now() - t0,\n service: \"castle-gateway\"\n });\n return;\n }\n const persistence = getFirebasePersistence();\n let firestoreOk = persistence.mode !== \"firebase\";\n let firestoreDetail = persistence.mode === \"file\" ? \"file_mode\" : \"not_applicable\";\n if (persistence.mode === \"firebase\" && persistence.db) {\n const pr = await probeFirestoreReachable(persistence.db, 2000);\n firestoreOk = pr.ok;\n firestoreDetail = pr.reason || (pr.ok ? \"reachable\" : \"fail\");\n } else if (persistence.mode === \"firebase\" && !persistence.db) {\n firestoreOk = false;\n firestoreDetail = \"firebase_not_initialized\";\n }\n const llmOk = rhizohLlmEnvConfigured();\n const overallOk = llmOk && firestoreOk;\n const latencyMs = Date.now() - t0;\n sendJson(res, 200, {\n ok: overallOk,\n dns: true,\n llm: llmOk,\n firestore: persistence.mode === \"firebase\" ? firestoreOk : false,\n persistence: persistence.mode,\n firestoreDetail,\n latencyMs,\n service: \"castle-gateway\",\n wsPort: PORT\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health\") {\n const persistence = getFirebasePersistence();\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n wsPort: PORT,\n persistence: persistence.mode,\n presenceMesh: \"sse\"\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/health\") {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n sendJson(res, 200, {\n ok: true,\n role: \"gateway\",\n queueLagMs: infraMetrics.queueLag,\n queueDepth: infraMetrics.queueDepth,\n errors: infraMetrics.errors,\n status: scored.status,\n score: scored.score,\n reasons: scored.reasons,\n worker: worker\n ? {\n status: worker.status,\n score: worker.score,\n reasons: worker.reasons\n }\n : null,\n timestamp: Date.now()\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisRuntime) {\n const payload = await buildGenesisRuntimeSurfacePayloadLive();\n sendJson(res, 200, payload);\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.genesisIngress) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const actor = await resolveMeshActor(req);\n const clientId = actor.ok ? actor.clientUid : (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestGenesisWorldObservationV0(body, { clientId });\n if (!result.ok) {\n const status =\n result.error === \"rate_limited\" || result.error === \"agent_spoke_throttled\"\n ? 429\n : result.error === \"payload_too_large\"\n ? 413\n : 400;\n sendJson(res, status, result);\n return;\n }\n sendJson(res, result.idempotent ? 200 : 202, result);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisStream) {\n const corsOrigin = accessControlAllowOriginValue(req);\n if (!corsOrigin && String(process.env.NODE_ENV || \"\").toLowerCase() === \"production\") {\n const og = String(req.headers?.origin || \"\").trim();\n if (og) {\n console.warn(\"[GATEWAY] genesis SSE: missing Access-Control-Allow-Origin for browser Origin (CORS whitelist mismatch):\", og);\n }\n }\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n res.write(\"retry: 3000\\n\\n\");\n res.write(\": genesis continuity stream\\n\\n\");\n res.write(\"event: genesis\\n\");\n res.write(`data: {\"booted\":true,\"pid\":${process.pid}}\\n\\n`);\n registerGenesisSseClient(res, req);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLatest) {\n const cp = getLatestGenesisSignedCheckpoint();\n if (!cp) {\n sendJson(res, 404, { ok: false, error: \"no_checkpoint_yet\", hint: \"wait_for_seq_interval_or_configure_signing_secret\" });\n return;\n }\n sendJson(res, 200, { ok: true, checkpoint: cp });\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(rhizohRuntime.routes.genesisCheckpointBySeqPrefix)) {\n const tail = pathname.slice(rhizohRuntime.routes.genesisCheckpointBySeqPrefix.length).split(\"/\")[0];\n const seq = parseInt(String(tail || \"\").trim(), 10);\n const out = await genesisCheckpointQueryBySeqV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"checkpoint_not_found\") {\n sendJson(res, 404, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointRange) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryRangeV0(from, to);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"range_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLineage) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const seq = parseInt(String(u.searchParams.get(\"seq\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryLineageV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"lineage_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisContinuityEvents) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const limit = parseInt(String(u.searchParams.get(\"limit\") || \"\").trim(), 10);\n const out = await queryGenesisContinuityEventArchiveV0(from, to, type, limit);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\" || out.error === \"event_archive_disabled\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"archive_file_too_large\") {\n sendJson(res, 413, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplay) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await resolveGenesisReplayRouterV1({\n from: fromSeq,\n to: toSeq,\n type,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayDiff) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const out = await computeGenesisReplayTemporalDiffV1({ from: fromSeq, to: toSeq, type });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEquivalence) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from1 = parseInt(String(u.searchParams.get(\"from1\") || \"\").trim(), 10);\n const to1 = parseInt(String(u.searchParams.get(\"to1\") || \"\").trim(), 10);\n const from2 = parseInt(String(u.searchParams.get(\"from2\") || \"\").trim(), 10);\n const to2 = parseInt(String(u.searchParams.get(\"to2\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await compareGenesisReplayEquivalenceV1({\n from1,\n to1,\n from2,\n to2,\n type,\n includeCheckpoints\n });\n if (\n !out.ok &&\n (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")\n ) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayAnalytics) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayAnalyticsV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEvolution) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const collapseWindows = parseInt(String(u.searchParams.get(\"collapseWindows\") || \"0\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayEvolutionV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n collapseWindows,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/metrics\") {\n res.writeHead(200, { \"Content-Type\": \"text/plain; version=0.0.4; charset=utf-8\" });\n res.end(renderInfraMetricsProm());\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.infraPrometheusMetrics) {\n if (process.env.CASTLE_PROMETHEUS_METRICS !== \"1\") {\n sendJson(res, 404, { ok: false, error: \"prometheus_metrics_disabled\" });\n return;\n }\n res.writeHead(200, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(renderRhizohPrometheusMetrics());\n return;\n }\n if (req.method === \"GET\" && pathname.startsWith(\"/infra/traces/\")) {\n const traceId = decodeURIComponent(pathname.slice(\"/infra/traces/\".length));\n const item = await getTraceById(traceId);\n if (!item) return sendJson(res, 404, { ok: false, error: \"trace_not_found\" });\n return sendJson(res, 200, { ok: true, trace: item });\n }\n if (req.method === \"GET\" && pathname === \"/infra/traces\") {\n const u = new URL(String(req.url || \"/infra/traces\"), `http://${req.headers.host || \"localhost\"}`);\n const sessionId = String(u.searchParams.get(\"sessionId\") || \"\").trim();\n const items = sessionId ? await listTracesBySession(sessionId) : [];\n return sendJson(res, 200, { ok: true, sessionId, traces: items });\n }\n\n /** Presence room mesh (C): JOIN / LEAVE / SNAPSHOT / DELTA / REPLAY (HTTP); SUBSCRIBE = SSE stream. */\n if (req.method === \"POST\" && pathname === \"/presence/mesh/join\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid, clientUid: actor.clientUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/leave\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshLeave(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/snapshot\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n sendJson(res, 200, meshSnapshot(roomUid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/delta\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, MAX_MESSAGE_BYTES);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const seq = meshAppendDelta(roomUid, actor.clientUid, {\n node: body?.node,\n projectionPatch: body?.projectionPatch,\n writerSubject: body?.writerSubject\n });\n sendJson(res, 200, { ok: true, roomUid, seq });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/replay\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const fromNodeId = body?.fromNodeId != null ? String(body.fromNodeId) : \"\";\n const fromSeq = body?.fromSeq;\n if (fromNodeId) {\n sendJson(res, 200, meshReplayFromNodeId(roomUid, fromNodeId));\n return;\n }\n sendJson(res, 200, meshReplayFromSeq(roomUid, fromSeq));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/presence/mesh/subscribe\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const roomUid = String(q.searchParams.get(\"roomUid\") || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n const write = (chunk) => {\n try {\n res.write(chunk);\n } catch {\n /* closed */\n }\n };\n const unsub = meshSubscribeSse(roomUid, write);\n const onClose = () => {\n unsub();\n meshLeave(roomUid, actor.clientUid);\n try {\n res.end();\n } catch {\n /* ignore */\n }\n };\n req.on(\"close\", onClose);\n req.on(\"aborted\", onClose);\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, uid: auth.uid, mode: auth.mode, items: await listConnections(auth.uid) });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const id = await createConnection(auth.uid, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"create_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n await updateConnection(auth.uid, id, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"update_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"DELETE\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n await deleteConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"delete_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url?.startsWith(\"/rhizoh/network/presence\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const u = new URL(req.url, \"http://localhost\");\n const roomKey = String(u.searchParams.get(\"room\") || \"world_space_c2c_v0\").slice(0, 64);\n sendJson(res, 200, {\n ok: true,\n roomKey,\n presence: listCastleNetworkPresenceV0(roomKey)\n });\n } catch (error) {\n sendJson(res, 500, { ok: false, error: error?.message || \"presence_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/rhizoh/sync/vault\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const snapshot = await getCloudSyncSnapshotV0(auth.uid);\n sendJson(res, 200, { ok: true, uid: auth.uid, snapshot });\n } catch (error) {\n sendJson(res, 500, { ok: false, error: error?.message || \"cloud_sync_pull_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/rhizoh/sync/vault\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const snapshot = await mergeCloudSyncSnapshotV0(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, uid: auth.uid, snapshot });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"cloud_sync_push_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections/test\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n if (!checkHttpRateLimit(`llm_conn_test:${auth.uid}`, RL_LLM_CONN_TEST_PER_MIN, 60_000)) {\n return sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n }\n const hasBodyKey = !!String(payload?.apiKey || \"\").trim();\n const result = await queryRhizohLlm(\n {\n message: String(payload?.message || \"Ping from Castle Gateway\"),\n context: payload?.context || { source: \"connection_test\" },\n provider: payload?.provider,\n model: payload?.model,\n apiKey: payload?.apiKey || \"\"\n },\n { allowExternalApiKey: true, llmKeySource: hasBodyKey ? \"auto\" : \"env\" }\n );\n logLlmAccess({\n route: \"/llm/connections/test\",\n uid: auth.uid,\n llmKeyBillingOwner: result.llmKeyBillingOwner,\n llmKeyOrigin: result.llmKeyOrigin,\n provider: result.provider,\n model: result.model,\n connectionId: null\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"connection_test_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAgentIdentities(auth.uid);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerAgentIdentity({\n ownerUid: auth.uid,\n agentId: payload?.agentId,\n role: payload?.role,\n personaSeed: payload?.personaSeed || {},\n capabilityLevel: payload?.capabilityLevel\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/agents/identities/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const owns = await getAgentIdentity(id);\n if (!owns || owns.ownerUid !== auth.uid) return sendJson(res, 403, { ok: false, error: \"forbidden\" });\n const payload = await readHttpJson(req);\n const row = await updateAgentIdentity(id, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/academy/events\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/academy/events\"}`);\n const status = String(url.searchParams.get(\"status\") || \"\");\n const items = await listAcademyEvents({ ownerUid: auth.uid, status, limit: 120 });\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/academy/events\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const ev = await queueAcademyEvent({\n type: payload?.type,\n roomId: payload?.roomId,\n ownerUid: auth.uid,\n participants: payload?.participants || [],\n topic: payload?.topic || \"academy\",\n payload: payload?.payload || {}\n });\n sendJson(res, 200, { ok: true, event: ev });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && String(req.url || \"\").startsWith(\"/academy/events/\") && String(req.url || \"\").endsWith(\"/resolve\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const parts = String(req.url || \"\").split(\"/\").filter(Boolean);\n const eventId = parts[2] || \"\";\n try {\n const payload = await readHttpJson(req);\n const out = await resolveAcademyEvent(eventId, payload || {});\n sendJson(res, 200, { ok: true, event: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_resolve_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/memory\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/memory\"}`);\n const scope = url.searchParams.get(\"scope\") === \"agents\" ? \"agents\" : \"users\";\n const id = String(url.searchParams.get(\"id\") || (scope === \"agents\" ? \"\" : auth.uid));\n const kind = String(url.searchParams.get(\"kind\") || \"\");\n const limit = Number(url.searchParams.get(\"limit\") || 40);\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n sendJson(res, 200, { ok: true, scope, id, kind, items: await listMemories({ scope, id, kind, limit }) });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const profile = await getPersonaGoalMemory(auth.uid);\n sendJson(res, 200, { ok: true, profile });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const profile = await setPersonaGoalMemory({\n uid: auth.uid,\n personaPatch: payload?.personaPatch || {},\n goals: Array.isArray(payload?.goals) ? payload.goals : null,\n preferencesPatch: payload?.preferencesPatch || {}\n });\n sendJson(res, 200, { ok: true, profile });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"profile_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/compact\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const out = await autoCompactMemories({ scope, id });\n sendJson(res, 200, { ok: true, scope, id, ...out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_compact_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/context\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const context = await getMemoryContext({\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n query: String(payload?.query || \"\"),\n limit: Number(payload?.limit || 80)\n });\n sendJson(res, 200, { ok: true, context });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_context_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/place-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const placeName = String(payload?.placeName || \"\").trim();\n if (!placeName) return sendJson(res, 400, { ok: false, error: \"placeName_required\" });\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const persona = String(payload?.waypointPersona || payload?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(placeName)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Place: ${placeName}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Give a concise conversational briefing about this place for a walking-route companion.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-place-brief\", placeName, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `PLACE_BRIEF:${placeName} => ${String(summary.reply || \"\").slice(0, 900)}`,\n tags: [\"place\", \"brief\", \"event-layer\"],\n importance: 0.62,\n meta: { placeName }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"place-brief\",\n text: `${placeName} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: {\n placeName,\n directive: summary.directive,\n lat: hit?.lat ? Number(hit.lat) : null,\n lon: hit?.lon ? Number(hit.lon) : null\n }\n });\n sendJson(res, 200, { ok: true, place: hit || null, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"place_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/route-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const waypoints = Array.isArray(payload?.waypoints) ? payload.waypoints.slice(0, 24) : [];\n const out = [];\n for (const wp of waypoints) {\n const name = String(wp?.name || \"\").trim();\n if (!name) continue;\n const persona = String(wp?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(name)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Waypoint: ${name}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Generate a concise spoken briefing for route waypoint.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-route-brief\", waypoint: name, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n out.push({\n name,\n persona,\n place: hit || null,\n reply: summary.reply,\n directive: summary.directive\n });\n }\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `ROUTE_BRIEF: waypoints=${out.length} ${out.map((x) => `${x.name}:${x.persona}`).join(\" | \")}`.slice(0, 1300),\n tags: [\"route\", \"waypoint\", \"brief\", \"event-layer\"],\n importance: 0.74,\n meta: { count: out.length }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"route-brief\",\n text: out.map((x) => `${x.name}:${String(x.reply || \"\").slice(0, 140)}`).join(\" | \").slice(0, 1400),\n roomId: \"studio-main\",\n meta: {\n waypointCount: out.length,\n waypoints: out.map((x) => ({\n name: x.name,\n persona: x.persona,\n lat: x.place?.lat ? Number(x.place.lat) : null,\n lon: x.place?.lon ? Number(x.place.lon) : null\n }))\n }\n });\n sendJson(res, 200, { ok: true, items: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"route_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/pdf-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const title = String(payload?.title || \"document\");\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const providedText = String(payload?.text || \"\");\n const url = String(payload?.url || \"\");\n let content = providedText;\n if (!content && url) content = await fetchTextFromUrl(url);\n if (!content) return sendJson(res, 400, { ok: false, error: \"text_or_supported_url_required\" });\n const summary = await queryRhizohLlm(\n {\n message: `Summarize this document and provide key points + practical actions.\\nTitle:${title}\\nContent:\\n${content.slice(0, 12000)}`,\n context: { source: \"event-layer-pdf-brief\", title },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `DOC_BRIEF:${title} => ${String(summary.reply || \"\").slice(0, 1000)}`,\n tags: [\"document\", \"brief\", \"library\", \"event-layer\"],\n importance: 0.7,\n meta: { title, url: url || null }\n });\n await addTranscript(auth.uid, {\n source: \"library-brief\",\n eventType: \"pdf-brief\",\n text: `${title} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { title }\n });\n sendJson(res, 200, { ok: true, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/pdf/upload\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req, 8 * 1024 * 1024);\n const job = await enqueuePdfJob({\n uid: auth.uid,\n fileName: payload?.fileName || \"document.pdf\",\n bytesBase64: payload?.contentBase64\n });\n sendJson(res, 200, { ok: true, job });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_upload_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/session\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createPublishSession(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session\",\n text: `Publish session armed: ${row.protocol} ${row.target}`.slice(0, 500),\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: row.id, bridge: row.bridge }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/studio/publish/sessions\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listPublishSessions(auth.uid, 80);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/health-check\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const protocol = String(payload?.protocol || \"WHIP\");\n const target = String(payload?.target || \"\");\n const health = await checkPublishTargetHealth(protocol, target);\n if (payload?.sessionId) {\n await updatePublishSession(auth.uid, String(payload.sessionId), {\n status: health.ok ? \"healthy\" : \"degraded\",\n reconnectPolicy: {\n enabled: true,\n backoffSec: [2, 5, 10, 20],\n nextRetryInSec: health.reconnectInSec || 0,\n maxRetries: 8,\n reason: health.reason\n }\n });\n }\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-health\",\n text: `${protocol} ${target} => ${health.ok ? \"healthy\" : \"degraded\"} (${health.reason})`,\n roomId: \"studio-main\",\n meta: { protocol, target, reconnect: !!health.shouldReconnect }\n });\n sendJson(res, 200, { ok: true, health });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_health_check_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/studio/publish/sessions/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updatePublishSession(auth.uid, id, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session-update\",\n text: `Session ${id} -> ${row.status || \"updated\"}`,\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: id, status: row.status, ingestUrl: row.ingestUrl || \"\" }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/transcripts\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/transcripts\"}`);\n const limit = Math.max(1, Math.min(200, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await listTranscripts(auth.uid, limit);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/transcripts\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await addTranscript(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"transcript_write_failed\" });\n }\n return;\n }\n\n /** E1: broadcast intent → GreenRoom capability → transcript + memory artifact */\n if (req.method === \"POST\" && req.url === \"/studio/capabilities/greenroom\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const traceId = String(payload?.traceId || `TRC-${Date.now().toString(36).toUpperCase()}`);\n const title = String(payload?.title || \"Castle Live Broadcast\").slice(0, 200);\n const intentRaw = String(payload?.intentRaw || \"\").slice(0, 800);\n const audienceEstimate = Math.max(0, Math.min(50_000_000, Number(payload?.audienceEstimate) || 1200));\n const roomId = String(payload?.roomId || \"greenroom-main\");\n const transcriptRow = await addTranscript(auth.uid, {\n source: \"rhizoh\",\n eventType: \"BROADCAST_ROUTED\",\n text: `GreenRoom · ${title}${intentRaw ? `\\nIntent: ${intentRaw}` : \"\"}`,\n roomId,\n meta: {\n ack: \"BROADCAST_ROUTED\",\n capability: \"greenroom\",\n intentType: \"broadcast\",\n traceId,\n audienceEstimate,\n artifact: {\n kind: \"broadcast_card\",\n title,\n traceId,\n roomId,\n createdAt: Date.now()\n }\n }\n });\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n text: `GREENROOM_BROADCAST:${title} trace=${traceId}`,\n tags: [\"broadcast\", \"greenroom\", \"artifact\", \"castle-library\"],\n importance: 0.72,\n kind: \"episodic\",\n meta: { traceId, capability: \"greenroom\", transcriptId: transcriptRow.id }\n });\n sendJson(res, 200, {\n ok: true,\n ack: \"BROADCAST_ROUTED\",\n traceId,\n transcript: transcriptRow,\n replayPath: `/replay/${encodeURIComponent(traceId)}`,\n sharePath: `/replay/${encodeURIComponent(traceId)}`\n });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"greenroom_capability_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/metadata/search\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/metadata/search\"}`);\n const q = String(url.searchParams.get(\"q\") || \"\");\n const limit = Math.max(1, Math.min(300, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await searchMetadata(auth.uid, q, limit);\n sendJson(res, 200, { ok: true, q, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerDevice(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, device: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"device_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const lat = Number(payload?.lat);\n const lon = Number(payload?.lon);\n const speed = Math.max(0, Number(payload?.speed || 0));\n const cfgCenterLat = Number(process.env.CASTLE_GEOFENCE_CENTER_LAT || 41.0082);\n const cfgCenterLon = Number(process.env.CASTLE_GEOFENCE_CENTER_LON || 28.9784);\n const cfgRadiusM = Math.max(100, Number(process.env.CASTLE_GEOFENCE_RADIUS_M || 55000));\n const cfgSpeedCap = Math.max(1, Number(process.env.CASTLE_SPEED_CAP_MPS || 35));\n const dist = Number.isFinite(lat) && Number.isFinite(lon) ? haversineMeters(cfgCenterLat, cfgCenterLon, lat, lon) : 0;\n const geofenceOk = !Number.isFinite(dist) ? true : dist <= cfgRadiusM;\n const speedOk = speed <= cfgSpeedCap;\n const failsafe = !(geofenceOk && speedOk);\n const safety = {\n geofenceOk,\n speedOk,\n failsafe,\n distanceFromCenterM: Number.isFinite(dist) ? dist : null,\n speedCapMps: cfgSpeedCap\n };\n const row = await appendTelemetry(auth.uid, deviceId, {\n lat,\n lon,\n speed,\n battery: payload?.battery,\n mode: payload?.mode || (failsafe ? \"FAILSAFE_HOLD\" : \"ACTIVE\"),\n raw: payload?.raw || {}\n });\n await updateDevice(auth.uid, deviceId, {\n status: failsafe ? \"failsafe\" : \"active\",\n lastTelemetryAt: row.ts,\n safetyState: safety\n });\n if (failsafe) {\n await addTranscript(auth.uid, {\n source: \"robotics-guardrails\",\n eventType: \"failsafe\",\n text: `Device ${deviceId} failsafe triggered geofenceOk=${geofenceOk} speedOk=${speedOk}`,\n roomId: \"studio-main\",\n meta: { deviceId, safety }\n });\n }\n sendJson(res, 200, { ok: true, telemetry: row, safety, command: failsafe ? \"FAILSAFE_HOLD\" : \"CONTINUE\" });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"telemetry_ingest_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listTelemetry(auth.uid, 150);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createAutomation(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"automation-scheduled\",\n text: `Automation scheduled: ${row.title} at ${new Date(row.scheduleAt).toLocaleString()}`,\n roomId: \"studio-main\",\n meta: { automationId: row.id, action: row.action, deviceId: row.deviceId }\n });\n sendJson(res, 200, { ok: true, automation: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"automation_create_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAutomations(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listDevices(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const devs = await listDevices(auth.uid, 500);\n const d = devs.find((x) => x.id === deviceId);\n if (!d) return sendJson(res, 404, { ok: false, error: \"device_not_found\" });\n const action = String(payload?.action || \"\");\n const caps = Array.isArray(d.capabilityProfile) ? d.capabilityProfile : [];\n if (action && caps.length && !caps.includes(action) && !caps.includes(\"*\")) {\n return sendJson(res, 400, { ok: false, error: \"unsupported_action_for_device\", capabilityProfile: caps });\n }\n const cmd = await enqueueCommand(auth.uid, {\n deviceId,\n action: action || \"NOOP\",\n params: payload?.params || {},\n adapter: payload?.adapter || d.adapter || \"websocket\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-command-queue\",\n eventType: \"command-queued\",\n text: `${cmd.deviceId} -> ${cmd.action}`,\n roomId: \"studio-main\",\n meta: { commandId: cmd.id, adapter: cmd.adapter }\n });\n sendJson(res, 200, { ok: true, command: cmd });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listCommands(auth.uid, 200);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/robotics/commands/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updateCommandStatus(auth.uid, id, payload?.status || \"sent\");\n sendJson(res, 200, { ok: true, command: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/rhizoh/bridge\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const result = await runRhizohBrainV2({\n message: String(payload?.message || \"robotics bridge command\"),\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n worldState: payload?.worldState || {},\n deviceState: payload?.deviceState || {},\n intent: \"robotics-mechanics\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-bridge\",\n eventType: \"robotics-command\",\n text: `Bridge -> ${String(result.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { directive: result.directive, agentId: payload?.agentId || \"\" }\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"robotics_bridge_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/planner/graph\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const graph = buildTaskActionGraph(String(payload?.task || \"\"), payload?.context || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"planner-graph\",\n text: `Planner graph built: ${graph.taskTitle} nodes=${graph.nodes.length}`,\n roomId: \"studio-main\",\n meta: { taskTitle: graph.taskTitle, nodeCount: graph.nodes.length }\n });\n sendJson(res, 200, { ok: true, graph });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"planner_graph_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerSocialChannel(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, channel: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_channel_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listSocialChannels(auth.uid, 100);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/broadcast\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const text = String(payload?.text || \"\").slice(0, 1200);\n const channels = await listSocialChannels(auth.uid, 100);\n const enabled = channels.filter((c) => c.status === \"enabled\");\n const deliveries = [];\n for (const c of enabled) {\n const out = await deliverSocialWithRetry(c, text);\n deliveries.push({ id: c.id, platform: c.platform, ...out });\n }\n const okCount = deliveries.filter((d) => d.ok).length;\n await addTranscript(auth.uid, {\n source: \"social-layer\",\n eventType: \"social-broadcast\",\n text: `Broadcast delivered ${okCount}/${enabled.length} channel(s): ${text.slice(0, 240)}`,\n roomId: \"studio-main\",\n meta: { deliveries }\n });\n sendJson(res, 200, { ok: true, sentChannels: okCount, totalChannels: enabled.length, deliveries });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_broadcast_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/ethics/programs\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, ...buildEthicsPrograms() });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands/dispatch\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const all = await listCommands(auth.uid, 200);\n const queued = all.filter((c) => String(c.status || \"queued\") === \"queued\").slice(0, 50);\n const devices = await listDevices(auth.uid, 500);\n const byId = new Map(devices.map((d) => [d.id, d]));\n const results = [];\n for (const cmd of queued) {\n const dev = byId.get(cmd.deviceId);\n if (!dev) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: \"device_not_found\" });\n continue;\n }\n try {\n const dispatched = await dispatchCommandToAdapter(dev, cmd);\n await updateCommandStatus(auth.uid, cmd.id, \"sent\");\n results.push({ id: cmd.id, ok: true, adapter: dispatched.adapter, via: dispatched.via });\n } catch (error) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: String(error?.message || \"dispatch_failed\") });\n }\n }\n await addTranscript(auth.uid, {\n source: \"robotics-command-dispatch\",\n eventType: \"command-dispatch-batch\",\n text: `Dispatch batch size=${results.length} ok=${results.filter((x) => x.ok).length}`,\n roomId: \"studio-main\",\n meta: { results: results.slice(0, 30) }\n });\n sendJson(res, 200, { ok: true, processed: results.length, results });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"dispatch_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/pdf/jobs/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n const job = await getPdfJob(auth.uid, id);\n if (!job) return sendJson(res, 404, { ok: false, error: \"pdf_job_not_found\" });\n sendJson(res, 200, { ok: true, job });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const row = await appendMemory({\n scope,\n id,\n text: payload?.text,\n tags: payload?.tags || [],\n importance: payload?.importance,\n kind: payload?.kind || \"episodic\",\n meta: { uid: auth.uid, ...(payload?.meta || {}) }\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.externalTruth) {\n try {\n if (process.env.RHIZOH_EXTERNAL_TRUTH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_truth:${ip}`, RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n sendJson(res, 200, buildRhizohExternalGroundTruthPayload(req));\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.productOutcome) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const rawBody = await readHttpBodyText(req, 64 * 1024);\n const body = rawBody.trim() ? safeJsonParse(rawBody) : null;\n if (!body || typeof body !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"invalid_json_payload\" });\n return;\n }\n const sigHdr = String(req.headers?.[\"x-rhizoh-outcome-signature\"] || \"\").trim();\n const hmacV = verifyRhizohOutcomeHmac(rawBody, sigHdr);\n if (!hmacV.ok) {\n sendJson(res, 401, { ok: false, error: hmacV.error || \"outcome_hmac_invalid\" });\n return;\n }\n const srcTokHdr = String(req.headers?.[\"x-rhizoh-outcome-source-token\"] || \"\").trim();\n const srcV = verifyRhizohOutcomeSourceToken(body.source, srcTokHdr);\n if (!srcV.ok) {\n sendJson(res, 401, { ok: false, error: srcV.error || \"outcome_source_token_invalid\" });\n return;\n }\n const subjectKey = String(body.subjectKey || \"\").trim().slice(0, 128);\n if (\n subjectKey &&\n !checkHttpRateLimit(\n `rhizoh_product_outcome_subj:${subjectKey}`,\n RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN,\n 60_000\n )\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_subject\" });\n return;\n }\n const result = ingestRhizohProductOutcomeHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n schemaVersion: \"1.1.0\",\n aggregate: result.aggregate,\n integrity: result.integrity\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"outcome_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.productOutcomeAggregate) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome_agg:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const u = new URL(String(req.url || \"/\"), \"http://localhost\");\n const cohortId = u.searchParams.get(\"cohortId\") || \"\";\n const decisionFingerprint = u.searchParams.get(\"decisionFingerprint\") || \"\";\n const limit = u.searchParams.get(\"limit\") || \"50\";\n const rows = listRhizohProductOutcomeAggregates({\n cohortId: cohortId || undefined,\n decisionFingerprint: decisionFingerprint || undefined,\n limit: Number(limit) || 50\n });\n sendJson(res, 200, { ok: true, schemaVersion: \"1.1.0\", rows });\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.externalLossBatch) {\n try {\n if (process.env.RHIZOH_EXTERNAL_LOSS_BATCH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_loss_batch:${ip}`, RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const result = ingestRhizohExternalLossBatchHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, { ok: true, accepted: result.accepted ?? 0 });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"batch_failed\") });\n }\n return;\n }\n\n function moderationAdminOk(req) {\n const expected = String(process.env.CASTLE_MODERATION_ADMIN_KEY || \"\").trim();\n if (!expected) return false;\n const got = String(req.headers[\"x-castle-moderation-key\"] || \"\").trim();\n return got.length > 0 && got === expected;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.opsAbuseReport) {\n try {\n const body = await readHttpJson(req, 32 * 1024);\n const auth = await resolveHttpUser(req);\n const entry = submitAbuseReportV0({\n reporterUid: auth.ok ? auth.uid : null,\n targetUid: body?.targetUid ?? null,\n category: body?.category,\n detail: body?.detail,\n traceId: body?.traceId ?? null\n });\n sendJson(res, 202, { ok: true, reportId: entry.id, status: entry.status });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsModerationQueue) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n queue: listModerationQueueV0(100),\n softBlocked: listSoftBlockedUsersV0()\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-block\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const out = softBlockUserV0(body?.uid, body?.reason);\n sendJson(res, out.ok ? 200 : 400, out);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-unblock\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n sendJson(res, 200, removeSoftBlockV0(body?.uid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsAgentSnapshots) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n const limit = Number(new URL(req.url || \"\", \"http://local\").searchParams.get(\"limit\") || 50);\n sendJson(res, 200, { ok: true, snapshots: listRecentAgentSnapshotsV0(limit) });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.academicObservatoryExport) {\n const auth = await resolveHttpUser(req);\n const out = handleAcademicObservatoryExportGetV0(req, auth);\n sendJson(res, out.status, out.body);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsHardeningStatus) {\n const auth = await resolveHttpUser(req);\n const principal = auth.ok ? `uid:${auth.uid}` : null;\n const gclSnap = await getGlobalCostLedgerSnapshotV0(principal);\n const economicStrategy = evaluateEconomicStrategyL3V0(gclSnap);\n const hardeningTenantId = resolveHardeningTenantIdV0(auth, req);\n const unifiedState = await buildUnifiedStateNarrativeV0({\n tenantId: hardeningTenantId ?? undefined,\n platformScope: !hardeningTenantId,\n principal\n });\n sendJson(res, 200, {\n ok: true,\n schema: \"rhizoh.ops.hardening_status.v0\",\n containment: readAgentContainmentConfigV0(),\n cost: readCostContainmentConfigV0(),\n gcl: {\n backend: await getGclBackendStatusV0(),\n snapshot: gclSnap\n },\n economicStrategyL3: economicStrategy,\n phasedRollout: await getPhasedRolloutStatsV0(),\n tier: readPhasedRolloutTierV0(),\n principalCost: auth.ok ? await getCostContainmentStatsV0(principal) : null,\n stressTaxonomy: verifyStressTaxonomyCoverageV0(),\n stressConflictResolution: verifyStressConflictResolutionV0(),\n resolutionStabilityEnvelope: verifyResolutionStabilityEnvelopeV0(),\n unifiedState: {\n tenantScope: {\n tenantId: unifiedState.tenantScope?.tenantId,\n scope: unifiedState.tenantScope?.scope,\n noGlobalDerivedState: unifiedState.tenantScope?.noGlobalDerivedState\n },\n narrativeFingerprint: unifiedState.narrativeFingerprint,\n screenshotScopeWatermark: unifiedState.screenshotScopeWatermark,\n interpretationSafetyContract: unifiedState.interpretationSafetyContract,\n interpretationUxContract: {\n schema: unifiedState.interpretationUxContract?.schema,\n layerOrder: unifiedState.interpretationUxContract?.layerOrder,\n narrativeNeverVisuallyDominant: unifiedState.interpretationUxContract?.narrativeNeverVisuallyDominant\n },\n governance: {\n narrativeIsNotDecisionLayer: unifiedState.governance.narrativeIsNotDecisionLayer,\n displayOrder: unifiedState.governance.displayOrder,\n trustPosture: unifiedState.governance.trustPosture,\n interpretationUxContract: unifiedState.governance.interpretationUxContract?.schema\n },\n humanOps: {\n decisionTier: unifiedState.humanOps?.humanDecisionScaling?.tier,\n decisionOwner: unifiedState.humanOps?.humanDecisionScaling?.decisionOwner,\n opsRouteId: unifiedState.humanOps?.humanDecisionOpsRunbook?.routing?.routeId,\n slaAckMinutes: unifiedState.humanOps?.humanDecisionOpsRunbook?.sla?.ackMinutes,\n uiDisclaimerRequired: unifiedState.humanOps?.narrativeUiSafety?.displayRules?.requireDisclaimerBanner,\n misreadHighResidual: unifiedState.humanOps?.misreadSimulation?.highResidualCount,\n propagationHighResidual: unifiedState.humanOps?.socialPropagationSimulation?.highResidualCount,\n worstWatermarkSurvivability:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.worstWatermarkSurvivability,\n dominantDistortionSource:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.dominantDistortionSource\n },\n culturalRisk: {\n perceptualRiskClass: unifiedState.culturalRisk?.perceptualRiskClass,\n trustFork: unifiedState.culturalRisk?.trustDynamics?.fork,\n trustDecayScore: unifiedState.culturalRisk?.trustDynamics?.scores?.trustDecay,\n mythologyScore: unifiedState.culturalRisk?.trustDynamics?.scores?.mythology,\n confidenceDetachmentExposure:\n unifiedState.culturalRisk?.trustDynamics?.axes?.confidenceDetachmentExposure\n },\n appliedSystemsLayer: {\n schema: unifiedState.appliedSystemsLayer?.schema,\n researchOutputType: unifiedState.appliedSystemsLayer?.researchGovernance?.outputType,\n roboticsBlockActuation: unifiedState.appliedSystemsLayer?.roboticsGrounding?.blockActuation,\n spiralRumorCount:\n unifiedState.appliedSystemsLayer?.spiralMMOGameKernel?.gameLayer?.rumors?.length\n },\n actionSemanticGovernance: {\n schema: unifiedState.actionSemanticGovernance?.schema,\n highRiskActionCount: unifiedState.actionSemanticGovernance?.highRiskActionCount,\n loopClosed: unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.loopClosed,\n authorityCollapseMitigated:\n unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.risks?.authorityCollapse\n ?.mitigated\n },\n actionContextResolution: {\n schema: unifiedState.actionContextResolution?.schema,\n contextFingerprint: unifiedState.actionContextResolution?.contextFingerprint?.fingerprintHash,\n domainCollision: unifiedState.actionContextResolution?.domainCollision?.detected,\n deployAgentSpiralBlocked:\n unifiedState.actionContextResolution?.actionOverloadingRisk?.deployAgentExample\n ?.spiral_mmo === \"blocked\",\n roboticsActuationEligible:\n unifiedState.actionContextResolution?.roboticsActuationCorrectness?.actuationEligible\n },\n epistemicCoherence: {\n schema: unifiedState.epistemicCoherence?.schema,\n verdict: unifiedState.epistemicCoherence?.systemCoherence?.verdict,\n coherenceScore: unifiedState.epistemicCoherence?.systemCoherence?.score,\n contradictionCount: unifiedState.epistemicCoherence?.crossLayerContradictions?.count,\n operatorDecision: unifiedState.epistemicCoherence?.uxCompression?.operatorDecision,\n headlineTr: unifiedState.epistemicCoherence?.uxCompression?.headline?.tr,\n falseSafetyWarning: Boolean(\n unifiedState.epistemicCoherence?.uxCompression?.falseSafetyWarning\n )\n },\n coherenceAuthorityBoundary: {\n schema: unifiedState.coherenceAuthorityBoundary?.schema,\n bindingValid: unifiedState.coherenceAuthorityBoundary?.eclAuthorityBinding?.valid,\n canExecute: unifiedState.coherenceAuthorityBoundary?.contract?.can_execute,\n mandatoryBannerTr:\n unifiedState.coherenceAuthorityBoundary?.disclaimers?.mandatoryBanner?.tr\n },\n decisionLatencyGovernance: {\n schema: unifiedState.decisionLatencyGovernance?.schema,\n latencyTier: unifiedState.decisionLatencyGovernance?.latencyTier,\n inflationScore: unifiedState.decisionLatencyGovernance?.latencyInflation?.score,\n fastPathEligible: unifiedState.decisionLatencyGovernance?.fastPath?.eligible,\n primaryActionId: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.primaryActionId,\n ackSlaMinutes: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.ackSlaMinutes,\n certaintyCap: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.certaintyCap,\n uncertaintyItems:\n unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.uncertaintyEnvelope?.itemCount\n },\n decisionPacketUncertaintyBoundary: {\n schema: unifiedState.decisionPacketUncertaintyBoundary?.schema,\n invariantValid: unifiedState.decisionPacketUncertaintyBoundary?.invariantCheck?.valid,\n centralInvariant: unifiedState.decisionPacketUncertaintyBoundary?.centralInvariant\n },\n epistemicDecisionPacing: {\n schema: unifiedState.epistemicDecisionPacing?.schema,\n queueSaturationTier:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation?.tier,\n operatorProcessingLatency:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation\n ?.operatorProcessingLatency,\n postureWindowCount:\n unifiedState.epistemicDecisionPacing?.temporaryStaticPostureWindows\n ?.temporaryStaticPostureWindows?.windowCount,\n executionBoundariesValid:\n unifiedState.epistemicDecisionPacing?.executionBoundaries?.valid,\n grantsExecutionApproval: unifiedState.epistemicDecisionPacing?.grantsExecutionApproval\n },\n epistemicTemporalCoherence: {\n schema: unifiedState.epistemicTemporalCoherence?.schema,\n allInvariantsValid: unifiedState.epistemicTemporalCoherence?.allInvariantsValid,\n temporalAlignmentValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.temporalAlignment?.valid,\n crossWindowGuardValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard?.valid,\n delayedTruthValid: unifiedState.epistemicTemporalCoherence?.invariants?.delayedTruthRule?.valid,\n reconciliationRequired:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard\n ?.reconciliation?.required\n },\n realityDriftObserver: {\n schema: unifiedState.realityDriftObserver?.schema,\n driftActive: unifiedState.realityDriftObserver?.driftObserverSummary?.active,\n misapprehensionShapeCount:\n unifiedState.realityDriftObserver?.misapprehensionShapeCatalog?.shapeCount,\n criticalShapeCount:\n unifiedState.realityDriftObserver?.driftObserverSummary?.criticalShapeCount,\n roboticsMismatchActive:\n unifiedState.realityDriftObserver?.roboticsFeedbackMismatch?.active\n },\n driftCausality: {\n schema: unifiedState.driftCausality?.schema,\n shapesWithCausalExplanation: unifiedState.driftCausality?.rdolLinkage?.shapesWithCausalExplanation,\n activeDomainCount: unifiedState.driftCausality?.causalitySummary?.activeDomainCount,\n propagationCausalityActive: unifiedState.driftCausality?.causalDomains?.propagation?.active\n },\n productReadiness: unifiedState.productReadiness,\n stateLayers: {\n raw: unifiedState.stateLayers.raw,\n derived: {\n systemState: unifiedState.stateLayers.derived.systemState,\n narrative: unifiedState.stateLayers.derived.narrative,\n validation: unifiedState.stateLayers.derived.validation\n },\n policy: {\n binding: unifiedState.stateLayers.policy.binding,\n decisionOwner: unifiedState.stateLayers.policy.decisionOwner,\n prohibitedActions: unifiedState.stateLayers.policy.prohibitedActions\n }\n },\n systemState: unifiedState.systemState,\n interpretation: unifiedState.interpretation,\n validation: {\n trustPosture: unifiedState.validation.trustPosture,\n adjustedConfidence: unifiedState.validation.adjustedConfidence,\n agreement: unifiedState.validation.confidenceDecomposition.agreement,\n divergenceScore: unifiedState.validation.divergence.divergenceScore,\n validated: unifiedState.validation.divergence.validated,\n uncertaintyTags: unifiedState.validation.uncertainty.tags.map((t) => t.id),\n flags: unifiedState.validation.divergence.flags.map((f) => f.id)\n },\n compression: unifiedState.compression\n }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 6 * 1024 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (\n !checkHttpRateLimit(`rhizoh_voice_transcribe:${rlKey}`, RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN, 60_000)\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const voiceEnv = rhizohVoiceTranscribeEnvV3();\n if (!voiceEnv.any) {\n sendJson(res, 503, {\n ok: false,\n error: \"voice_asr_not_configured\",\n env: voiceEnv\n });\n return;\n }\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n const result = await runRhizohVoiceTranscribeV3(\n applyLanguagePropagationToVoiceBodyV1(payload, langProp)\n );\n sendJson(res, result.ok ? 200 : 422, {\n ...result,\n traceId: String(payload?.traceId || \"\"),\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n atMs: Date.now()\n });\n } catch (error) {\n sendJson(res, 500, {\n ok: false,\n error: \"voice_transcribe_failed\",\n detail: String(error?.message || error),\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(\"/rhizoh/llm/task/\")) {\n try {\n const rawTaskId = decodeURIComponent(pathname.slice(\"/rhizoh/llm/task/\".length).split(\"/\")[0] || \"\");\n const taskId = String(rawTaskId || \"\").toLowerCase() === \"last\" ? getLatestLlmWorkerTaskIdV0() : rawTaskId;\n const snap = taskId ? getLlmWorkerTaskSnapshotV0(taskId) : null;\n if (!snap) {\n sendJson(res, 404, {\n ok: false,\n error: \"llm_task_not_found\",\n taskId: rawTaskId,\n status: \"missing\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n if (snap.status === \"processing\") {\n sendJson(res, 202, {\n ok: true,\n taskId: snap.taskId,\n status: \"processing\",\n createdAt: snap.createdAt,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n // Terminal states (completed/failed) always HTTP 200 — poll must not look like server fault (500).\n sendJson(res, 200, {\n ...(snap.body || { ok: false, error: \"llm_task_empty\", taskId: snap.taskId }),\n taskId: snap.taskId,\n status: snap.status === \"completed\" ? \"completed\" : snap.status,\n terminal: true,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n } catch (error) {\n console.error(\"[llm-task-poll] handler failed:\", error);\n sendJson(res, 200, {\n ok: false,\n error: \"llm_task_poll_handler_failed\",\n detail: String(error?.message || error),\n status: \"failed\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === rhizohRuntime.routes.rhizohLlm) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 128 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n logRhizohHealth(\"gateway_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: String(payload?.traceId || \"\"),\n auth: auth.ok ? \"ok\" : \"anon\",\n ip\n });\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (!checkHttpRateLimit(`rhizoh_llm:${rlKey}`, RL_RHIZOH_LLM_PER_MIN, 60_000)) {\n const rlTaxonomy = classifyStressResponseV0({ code: \"rate_limit_exceeded\" });\n return sendJson(res, 429, {\n ok: false,\n error: \"rate_limit_exceeded\",\n reply: rlTaxonomy.userMessageTr || \"İstek sınırı aşıldı. Kısa süre sonra tekrar deneyin.\",\n stressClass: rlTaxonomy.stressClass,\n responseAction: rlTaxonomy.responseAction,\n stressMatrix: rlTaxonomy.matrix,\n stressInterpretable: rlTaxonomy.interpretable,\n stressConfidence: rlTaxonomy.stressConfidence,\n actionConfidence: rlTaxonomy.actionConfidence,\n actionSoftened: rlTaxonomy.actionSoftened,\n responseActionStrict: rlTaxonomy.responseActionStrict,\n actionInterpretable: rlTaxonomy.actionInterpretable,\n conflictResolution: rlTaxonomy.conflictResolution,\n directive: \"NONE\"\n });\n }\n\n const requireExplicit = process.env.CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE === \"1\";\n const keyMode = normalizeClientLlmKeySource(payload?.llmKeySource ?? payload?.keySource);\n if (keyMode === null) {\n return sendJson(res, 400, {\n ok: false,\n error: \"invalid_llm_key_source\",\n reply: \"llmKeySource geçersiz. Kullanın: env | user_connection | auto\",\n directive: \"NONE\"\n });\n }\n if (requireExplicit && keyMode === \"auto\") {\n return sendJson(res, 400, {\n ok: false,\n error: \"llm_key_source_required\",\n reply: \"Sunucu CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE=1: istekte llmKeySource (env veya user_connection) zorunlu.\",\n directive: \"NONE\"\n });\n }\n\n const conn = auth.ok ? await resolveConnection(auth.uid, String(payload?.connectionId || \"\")) : null;\n\n if (keyMode === \"user_connection\") {\n if (!auth.ok) {\n return sendJson(res, 401, {\n ok: false,\n error: \"llm_auth_required_for_user_connection\",\n reply: \"Kayıtlı LLM bağlantısı için giriş gerekli.\",\n directive: \"NONE\"\n });\n }\n if (!conn?.apiKey) {\n return sendJson(res, 400, {\n ok: false,\n error: \"user_llm_connection_required\",\n reply: \"Geçerli bir LLM bağlantısı yok; Studio’da bağlantı ekleyin veya llmKeySource=env kullanın.\",\n directive: \"NONE\"\n });\n }\n }\n\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n let safePayload = applyLanguagePropagationToLlmPayloadV1({ ...(payload || {}) }, langProp);\n const trimOut = trimRhizohLlmGatewayPayloadV1(safePayload);\n if (trimOut.meta.trimmed) {\n logRhizohHealth(\"llm_payload_trimmed\", {\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n bytesBefore: trimOut.meta.bytesBefore,\n bytesAfter: trimOut.meta.bytesAfter\n });\n }\n safePayload = trimOut.payload;\n delete safePayload.apiKey;\n delete safePayload.llmKeySource;\n delete safePayload.keySource;\n\n const llmMessage = String(\n safePayload?.message || safePayload?.text || payload?.message || payload?.text || \"\"\n ).trim();\n if (!llmMessage) {\n return sendJson(res, 400, {\n ok: false,\n error: \"message_required\",\n reply: \"Mesaj metni gerekli (message alanı boş).\",\n directive: \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n if (!safePayload.message) {\n safePayload = { ...safePayload, message: llmMessage };\n }\n\n let resolvedProvider = payload?.provider;\n let resolvedModel = payload?.model;\n let connApiKey = \"\";\n\n ({\n resolvedProvider,\n resolvedModel,\n connApiKey\n } = resolveServerLlmProviderV0(keyMode, conn, payload));\n\n const turnInput = {\n safePayload,\n auth,\n keyMode,\n conn,\n resolvedProvider,\n resolvedModel,\n connApiKey\n };\n const turnMeta = {\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n langEcho: buildRhizohLanguagePropagationEchoV1(langProp)\n };\n\n if (\n isLlmWorkerEnabledV0() &&\n isLlmWorkerAsyncEnabledV0() &&\n String(req.headers[\"x-rhizoh-llm-sync\"] || \"\").trim() !== \"1\"\n ) {\n const taskId = enqueueLlmWorkerTaskAsyncV0(turnInput, turnMeta);\n logRhizohHealth(\"llm_worker_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n taskId,\n traceId: String(payload?.traceId || taskId),\n keyMode,\n async: true\n });\n sendJson(res, 202, {\n ok: true,\n taskId,\n status: \"processing\",\n traceId: String(payload?.traceId || taskId),\n pollPath: `/rhizoh/llm/task/${taskId}`,\n gatewayPid: process.pid,\n ...turnMeta.langEcho\n });\n return;\n }\n\n const { result, traceId, turnLatencyMs, spinePhases, sampledTrace } = isLlmWorkerEnabledV0()\n ? await runLlmWorkerTaskV0(turnInput, turnMeta)\n : await rhizohGatewayTurn(turnInput);\n logRhizohHealth(\"llm_response\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId,\n turnLatencyMs: Number(turnLatencyMs || 0),\n keyMode\n });\n sendJson(res, 200, {\n ...result,\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n traceId,\n turnLatencyMs,\n ...(spinePhases ? { spinePhases } : {}),\n ...(sampledTrace !== undefined ? { sampledTrace } : {})\n });\n } catch (error) {\n const msg = String(error?.message || \"\");\n const code = error?.code || \"\";\n let status = 500;\n if (msg === \"payload_too_large\") status = 413;\n else if (msg === \"rate_limit_exceeded\") status = 429;\n else if (\n code === \"cost_hard_limit\" ||\n code === \"phased_rollout_capacity\" ||\n error?.containment === true ||\n String(code).startsWith(\"agent_\")\n ) {\n status = 429;\n } else if (code === \"user_soft_blocked\" || code === \"prompt_abuse_detected\") status = 403;\n else if (\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(code) ||\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(msg) ||\n msg.includes(\"missing_api_key_for_\")\n )\n status = 400;\n let rhizohFailureKind = \"provider_error\";\n if (msg === \"rate_limit_exceeded\" || status === 429) rhizohFailureKind = \"rate_limit\";\n else if (status === 403) rhizohFailureKind = \"policy_block\";\n else if (status === 400 || status === 413) rhizohFailureKind = \"client_config\";\n else if (msg.startsWith(\"provider_http_\")) {\n const st = Number(msg.replace(\"provider_http_\", \"\"));\n if (st === 408 || st === 504) rhizohFailureKind = \"timeout\";\n else rhizohFailureKind = \"provider_error\";\n } else if (/timeout|timed out|ETIMEDOUT/i.test(msg)) rhizohFailureKind = \"timeout\";\n\n const providerHttpMatch = msg.match(/^provider_http_(\\d{3})$/);\n const stressTaxonomy =\n error?.stressClass != null\n ? {\n stressClass: error.stressClass,\n responseAction: error.responseAction,\n stressMatrix: error.stressMatrix,\n stressInterpretable: error.stressInterpretable !== false,\n stressConfidence: error.stressConfidence,\n actionConfidence: error.actionConfidence,\n actionSoftened: error.actionSoftened,\n responseActionStrict: error.responseActionStrict,\n actionInterpretable: error.actionInterpretable,\n conflictResolution: error.conflictResolution,\n stressSecondary: error.stressSecondary,\n responseActions: error.responseActions\n }\n : (() => {\n const t = classifyStressResponseV0({\n code: code || msg,\n rhizohFailureKind,\n providerHttpStatus: providerHttpMatch\n ? Number(providerHttpMatch[1])\n : null\n });\n return {\n stressClass: t.stressClass,\n responseAction: t.responseAction,\n stressMatrix: t.matrix,\n stressInterpretable: t.interpretable,\n stressConfidence: t.stressConfidence,\n actionConfidence: t.actionConfidence,\n actionSoftened: t.actionSoftened,\n responseActionStrict: t.responseActionStrict,\n actionInterpretable: t.actionInterpretable,\n conflictResolution: t.conflictResolution,\n stressSecondary: t.stressSecondary,\n responseActions: t.responseActions\n };\n })();\n sendJson(res, status, {\n ok: false,\n rhizohFailureKind,\n ...(providerHttpMatch ? { providerHttpStatus: Number(providerHttpMatch[1]) } : {}),\n error: code || msg || \"rhizoh_llm_failed\",\n detail: msg || code || undefined,\n reply:\n error?.reply ||\n (status === 500 ? \"Rhizoh bağlantısı geçici olarak kesildi.\" : msg),\n directive: error?.directive || \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n ...stressTaxonomy\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicSeal) {\n try {\n if (!EPISTEMIC_SEAL_SECRET) {\n sendJson(res, 503, { ok: false, error: \"epistemic_seal_disabled\" });\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`epistemic_seal:${ip}`, RL_EPISTEMIC_SEAL_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const truth = body.truth_contract;\n if (!truth || typeof truth !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"truth_contract_required\" });\n return;\n }\n const sealBody = {\n truth_contract: truth,\n runtime_hash: String(body.runtime_hash || \"\"),\n model_route: {\n provider: body.model_route?.provider ?? null,\n model: body.model_route?.model ?? null\n },\n memory_digest: String(body.memory_digest || \"\"),\n world_snapshot_hash: String(body.world_snapshot_hash || \"\"),\n timestamp: Number(body.timestamp) || Date.now()\n };\n const canonical = canonicalEpistemicSealString(sealBody);\n const { hash, signature } = hashAndSignEpistemicSeal(canonical, EPISTEMIC_SEAL_SECRET);\n recordGenesisEpistemicSealIssued(hash);\n const attestation = {\n schema: EPISTEMIC_SEAL_SCHEMA,\n algorithm: \"SHA-256+HMAC-SHA256\",\n keyHint: process.env.CASTLE_EPISTEMIC_SEAL_SECRET ? \"CASTLE_EPISTEMIC_SEAL_SECRET\" : \"CASTLE_GATEWAY_TOKEN\",\n issuedAt: Date.now(),\n gateway: { service: \"castle-gateway\", listenPort: PORT }\n };\n sendJson(res, 200, {\n ok: true,\n hash,\n signature,\n attestation,\n canonicalBytes: Buffer.byteLength(canonical, \"utf8\")\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"seal_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicLogsBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`epistemic_logs:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 120) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const persisted = await persistEpistemicLedgerBatch(auth.uid, entries);\n recordGenesisEpistemicLedgerPersisted(persisted.persisted);\n // Internal cognition layer: forecast stays gateway/ledger-only (no client exposure).\n try {\n await persistEpistemicForecastBatch(auth.uid, persisted.normalized || []);\n } catch {\n /* forecast failures must not break primary ledger ingest */\n }\n sendJson(res, 200, {\n ok: true,\n persisted: persisted.persisted,\n mode: persisted.mode,\n latest: persisted.normalized?.[persisted.normalized.length - 1] || null\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"epistemic_log_ingest_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.authorityLedgerBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`authority_ledger:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 80) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const witnessed = persistAuthorityLedgerWitnessBatchV1(auth.uid, entries, EPISTEMIC_SEAL_SECRET);\n if (witnessed.witnessed > 0) {\n recordGenesisEpistemicLedgerPersisted(witnessed.witnessed);\n const clientSeal = witnessed.lastWitness?.clientSealHash;\n if (clientSeal) recordGenesisEpistemicSealIssued(clientSeal);\n }\n sendJson(res, 200, {\n ok: true,\n witnessed: witnessed.witnessed,\n quarantined: witnessed.quarantined,\n chainHeight: witnessed.chainHeight,\n chainHead: witnessed.chainHead,\n lastWitness: witnessed.lastWitness,\n results: witnessed.results\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"authority_ledger_witness_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.authorityLedgerReplay) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const replayUrl = new URL(String(req.url || \"\"), \"http://localhost\");\n const epochId = String(replayUrl.searchParams.get(\"epochId\") || \"\").trim() || undefined;\n const snapshot = getAuthorityLedgerWitnessSnapshotV1(auth.uid, epochId);\n const replay = replayAuthorityLedgerWitnessChainV1(auth.uid, epochId);\n sendJson(res, 200, {\n ok: true,\n snapshot,\n replay,\n schema: \"castle.rhizoh.authority_ledger_witness.v1.replay_surface\"\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"authority_ledger_replay_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.authorityEpochMerge) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`authority_epoch_merge:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const merged = assimilateAuthorityEpochMergeV1(auth.uid, body);\n if (!merged.ok) {\n sendJson(res, 400, merged);\n return;\n }\n sendJson(res, 200, {\n ok: true,\n mergeEvent: merged.mergeEvent,\n recentMerges: merged.recentMerges\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"authority_epoch_merge_failed\") });\n }\n return;\n }\n\n sendJson(res, 404, { ok: false, error: \"not_found\" });\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\nspiralWssForGenesis = wss;\nconst orchestrator = createOrchestrator();\nconst clientStats = new Map();\nlet broadcasterClientId = null;\nconst spiralState = {\n rooms: [{ id: \"room-default\", name: \"Spiral Default Arena\" }],\n activeByClient: new Map(),\n characters: new Map() // id -> {id,name,role,roomId,source}\n};\n\n/** Castle social rooms — keyed by `castleRoomKey` from {@link WS_MESSAGE.CASTLE_SOCIAL_PULSE}. */\nconst castleSocialRoomState = new Map();\nconst CASTLE_SOCIAL_MEMBER_TTL_MS = 90_000;\nconst CASTLE_SOCIAL_MAX_PULSE_BYTES = 8192;\nconst RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES = 5 * 1024 * 1024;\nconst RHIZOH_VOICE_LIVE_MAX_CHUNKS = 96;\n\n/**\n * Voice Engine v3 live media lane.\n * Client streams MediaRecorder chunks over the existing gateway WS; STOP reuses the\n * gateway ASR runner so orchestration remains the single authority downstream.\n */\nconst rhizohVoiceLiveSessions = new WeakMap();\n\nfunction appendRhizohVoiceLiveEventV0(session, type, payloadRefSeed = \"\") {\n if (!session) return null;\n session.eventSeq = (Number(session.eventSeq) || 0) + 1;\n const event = createRhizohImmutableEventV0({\n type,\n sessionId: session.sessionId,\n traceId: session.traceId,\n actorSeed: session.sessionId || session.traceId,\n eventSeq: session.eventSeq,\n payloadRef: createRhizohPayloadRefV0(payloadRefSeed || `${session.sessionId}:${type}:${session.eventSeq}`)\n });\n session.events = [...(session.events || []), event].slice(-64);\n return event;\n}\n\nfunction sendRhizohVoiceLiveEnvelopeV0(socket, type, payload) {\n try {\n if (socket?.readyState === 1) socket.send(JSON.stringify(createEnvelope(type, payload)));\n } catch {\n /* noop */\n }\n}\n\nfunction decodeRhizohVoiceLiveChunkV0(payload = {}) {\n const b64 = String(payload.audioBase64 || \"\").trim();\n if (!b64) return { ok: false, error: \"audio_base64_required\" };\n try {\n const buf = Buffer.from(b64, \"base64\");\n if (!buf.length) return { ok: false, error: \"audio_decode_empty\" };\n return { ok: true, buffer: buf };\n } catch {\n return { ok: false, error: \"audio_base64_invalid\" };\n }\n}\n\nfunction handleRhizohVoiceLiveStartV0(socket, payload = {}) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const sessionId = String(p.sessionId || `live_${Date.now().toString(36)}`).slice(0, 96);\n rhizohVoiceLiveSessions.set(socket, {\n sessionId,\n traceId: String(p.traceId || \"\").slice(0, 128),\n mimeType: String(p.mimeType || \"audio/webm\").slice(0, 80),\n languageCode: String(p.languageCode || \"tr-TR\").slice(0, 32),\n path: String(p.path || \"both\").slice(0, 24),\n chunks: [],\n chunkIndexes: [],\n totalBytes: 0,\n nextChunkIndex: 1,\n eventSeq: 0,\n events: [],\n startedAtMs: Date.now()\n });\n appendRhizohVoiceLiveEventV0(rhizohVoiceLiveSessions.get(socket), \"VOICE_LIVE_SESSION_STARTED\", `${sessionId}:live_session`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_START, {\n ok: true,\n sessionId,\n schema: \"castle.rhizoh.voice_live.gateway_lane.v0\"\n });\n}\n\nfunction handleRhizohVoiceLiveChunkV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const sequence = validateRhizohVoiceLiveChunkPayloadV0(payload, {\n expectedChunkIndex: session.nextChunkIndex\n });\n if (!sequence.ok) {\n rhizohVoiceLiveSessions.delete(socket);\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:chunk_reject`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: sequence.error,\n expectedChunkIndex: sequence.expectedChunkIndex,\n chunkIndex: sequence.chunkIndex\n });\n return;\n }\n const decoded = decodeRhizohVoiceLiveChunkV0(payload);\n if (!decoded.ok) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: decoded.error\n });\n return;\n }\n if (\n session.chunks.length >= RHIZOH_VOICE_LIVE_MAX_CHUNKS ||\n session.totalBytes + decoded.buffer.length > RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES\n ) {\n rhizohVoiceLiveSessions.delete(socket);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_audio_too_large\"\n });\n return;\n }\n session.chunks.push(decoded.buffer);\n session.chunkIndexes.push(sequence.chunkIndex);\n session.totalBytes += decoded.buffer.length;\n session.nextChunkIndex += 1;\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_CHUNK_ACCEPTED\", `${session.sessionId}:chunk:${sequence.chunkIndex}`);\n}\n\nfunction handleRhizohVoiceLiveStopV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n rhizohVoiceLiveSessions.delete(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const lastChunkIndex = Number(payload?.lastChunkIndex);\n if (Number.isInteger(lastChunkIndex) && lastChunkIndex !== session.chunks.length) {\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:stop_mismatch`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_chunk_count_mismatch\",\n expectedChunkCount: lastChunkIndex,\n receivedChunkCount: session.chunks.length\n });\n return;\n }\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_STOP_RECEIVED\", `${session.sessionId}:stop:${session.chunks.length}`);\n const audio = Buffer.concat(session.chunks);\n if (!audio.length) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"audio_empty\"\n });\n return;\n }\n runRhizohVoiceTranscribeV3({\n path: String(payload?.path || session.path || \"both\"),\n audioBase64: audio.toString(\"base64\"),\n mimeType: String(payload?.mimeType || session.mimeType || \"audio/webm\"),\n languageCode: String(payload?.languageCode || session.languageCode || \"tr-TR\"),\n traceId: String(payload?.traceId || session.traceId || \"\"),\n sessionId: session.sessionId,\n transport: \"gateway_ws_live\"\n })\n .then((result) => {\n appendRhizohVoiceLiveEventV0(session, result?.ok ? \"VOICE_TRANSCRIBE_FINAL\" : \"VOICE_TRANSCRIBE_FAILED\", `${session.sessionId}:final`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_FINAL, {\n ...result,\n sessionId: session.sessionId,\n transportPath: \"gateway_ws_live\",\n bytes: audio.length,\n chunkCount: session.chunks.length,\n chunkSequence: Object.freeze({\n ok: true,\n firstChunkIndex: session.chunkIndexes[0] || null,\n lastChunkIndex: session.chunkIndexes[session.chunkIndexes.length - 1] || null,\n receivedChunkCount: session.chunks.length\n }),\n eventTrace: Object.freeze((session.events || []).slice(-16)),\n recordedMs: Math.max(0, Date.now() - session.startedAtMs)\n });\n })\n .catch((e) => {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_transcribe_failed\",\n detail: String(e?.message || e)\n });\n });\n}\n\nfunction removeCastleSocialClientFromAllRooms(clientId) {\n const cid = String(clientId || \"\");\n if (!cid) return;\n for (const room of castleSocialRoomState.values()) {\n room.members.delete(cid);\n }\n}\n\nfunction buildCastleSocialRoomPayload(roomKey) {\n const room = castleSocialRoomState.get(roomKey);\n if (!room) return null;\n const now = Date.now();\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const roster = [...room.members.values()].map((m) => ({\n userId: m.userId,\n lastMs: m.lastMs,\n ...m.sp\n }));\n const peerFeeds =\n room.walPeers && room.walPeers.size\n ? [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }))\n : [];\n return { castleRoomKey: roomKey, seq: room.seq, roster, peerFeeds, ts: now };\n}\n\nconst CASTLE_WAL_PEER_MAX_FEED_BYTES = 48 * 1024;\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {{ ok: boolean, code?: string, reason?: string }} gate\n */\nfunction rejectWalPeerIngressV0(socket, gate) {\n const code = gate.code || \"wal_peer_rejected\";\n recordWalPeerRejectV0(code);\n recordSubstrateOtelEventV0(\"wal_peer_reject\", { code, reason: String(gate.reason || \"\").slice(0, 120) });\n socket.send(\n JSON.stringify(\n createEnvelope(WS_MESSAGE.ERROR, {\n error: gate.reason || \"WAL peer ingress rejected.\",\n code\n })\n )\n );\n}\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {Record<string, unknown> | null | undefined} walPeerFeed\n * @returns {boolean}\n */\nfunction gateWalPeerIngressV0(socket, walPeerFeed) {\n const authGate = assertWalPeerSocketAuthorityV0(socket);\n if (!authGate.ok) {\n rejectWalPeerIngressV0(socket, authGate);\n return false;\n }\n const sigGate = validateWalPeerFeedSignatureV0(\n walPeerFeed && typeof walPeerFeed === \"object\" ? walPeerFeed : {}\n );\n if (!sigGate.ok) {\n rejectWalPeerIngressV0(socket, sigGate);\n return false;\n }\n return true;\n}\n\nfunction handleCastleWalPeerFeed(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_WAL_PEER_MAX_FEED_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_WAL_PEER_FEED too large.\" })));\n return;\n }\n const walPeerFeed = p.walPeerFeed && typeof p.walPeerFeed === \"object\" ? p.walPeerFeed : {};\n if (!gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const castleRoomKey = String(p.castleRoomKey || \"wal:convergence\").slice(0, 64);\n const castleId = String(p.castleId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId,\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: room.members.get(socket.clientId)?.sp ?? {}\n });\n const peerFeeds = [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }));\n const body = {\n castleRoomKey,\n seq: room.seq,\n peerFeeds,\n ts: now\n };\n const env = createEnvelope(WS_MESSAGE.CASTLE_WAL_PEER_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys(), ...room.walPeers.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction handleCastleSocialPulse(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_SOCIAL_MAX_PULSE_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_SOCIAL_PULSE too large.\" })));\n return;\n }\n const castleRoomKey = String(p.castleRoomKey || \"default\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n const sp = p.socialPulse && typeof p.socialPulse === \"object\" ? p.socialPulse : {};\n const walPeerFeed =\n p.walPeerFeed && typeof p.walPeerFeed === \"object\"\n ? p.walPeerFeed\n : sp.walPeerFeed && typeof sp.walPeerFeed === \"object\"\n ? sp.walPeerFeed\n : null;\n if (walPeerFeed && !gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const spNext = walPeerFeed ? { ...sp, walPeerFeed } : { ...sp };\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: spNext\n });\n if (walPeerFeed) {\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId: String(p.castleId || userId).slice(0, 64),\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n }\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const body = buildCastleSocialRoomPayload(castleRoomKey);\n if (!body) return;\n const env = createEnvelope(WS_MESSAGE.CASTLE_SOCIAL_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcast(msgObj) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcastState() {\n broadcast(\n createEnvelope(WS_MESSAGE.BROADCAST_STATE, {\n broadcasterClientId,\n viewers: [...wss.clients].filter((c) => c.readyState === 1).length\n })\n );\n}\n\nfunction currentSpiralStatePayload() {\n return {\n rooms: spiralState.rooms,\n characters: [...spiralState.characters.values()]\n };\n}\n\nfunction broadcastSpiralState() {\n broadcast(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload()));\n}\n\nfunction ensureRoom(roomId, fallbackName = null) {\n if (!roomId) return spiralState.rooms[0].id;\n const exists = spiralState.rooms.find((r) => r.id === roomId);\n if (exists) return exists.id;\n const created = { id: roomId, name: fallbackName || `Spiral-${roomId.slice(-4)}` };\n spiralState.rooms.push(created);\n return created.id;\n}\n\nfunction syncSpiralCharactersWithWorld(snapshot) {\n const agents = snapshot?.agents || [];\n for (const a of agents) {\n if (!spiralState.characters.has(a.id)) {\n spiralState.characters.set(a.id, {\n id: a.id,\n name: a.id,\n role: \"agent\",\n roomId: spiralState.rooms[0].id,\n source: \"world\"\n });\n }\n }\n}\n\nfunction broadcastExcept(msgObj, exceptSocket) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client !== exceptSocket && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction validateInputFrame(payload, socket) {\n const stats = clientStats.get(socket.clientId) || { lastFrame: 0, windowStart: Date.now(), msgCount: 0 };\n const now = Date.now();\n if (now - stats.windowStart > 5000) {\n stats.windowStart = now;\n stats.msgCount = 0;\n }\n stats.msgCount += 1;\n clientStats.set(socket.clientId, stats);\n if (stats.msgCount > 120) return \"Rate limit exceeded.\";\n\n if (!payload || !Number.isInteger(payload.frame)) return \"frame must be integer.\";\n if (payload.frame <= stats.lastFrame) return \"frame must be monotonic.\";\n const commands = Array.isArray(payload.commands) ? payload.commands : [];\n if (commands.length > 8) return \"Too many commands in one frame.\";\n for (const cmd of commands) {\n if (!cmd || typeof cmd.command !== \"string\") return \"Invalid command format.\";\n if (\n cmd.command !== \"SPAWN_AGENT\" &&\n cmd.command !== \"LIST_CASTLES\" &&\n cmd.command !== \"SPAWN_ENTITY\" &&\n cmd.command !== \"CREATE_CASTLE\"\n ) return \"Unsupported command.\";\n }\n stats.lastFrame = payload.frame;\n clientStats.set(socket.clientId, stats);\n return null;\n}\n\nfunction validateSignalPayload(payload) {\n if (!payload || typeof payload !== \"object\") return \"Invalid signaling payload.\";\n const signalType = payload.signalType;\n if (Object.values(CASTLE_NETWORK_SIGNAL_V0).includes(signalType)) {\n return null;\n }\n if (![\"OFFER\", \"ANSWER\", \"ICE\"].includes(signalType)) return \"Invalid signal type.\";\n if (typeof payload.to !== \"string\" || payload.to.length < 3) return \"Invalid signal destination.\";\n if (signalType !== \"ICE\" && JSON.stringify(payload.sdp || {}).length > 16 * 1024) return \"SDP too large.\";\n if (signalType === \"ICE\" && JSON.stringify(payload.candidate || {}).length > 6 * 1024) return \"ICE too large.\";\n return null;\n}\n\nfunction sanitizeRawMessage(raw) {\n if (!raw) return null;\n const text = raw.toString();\n if (Buffer.byteLength(text, \"utf8\") > MAX_MESSAGE_BYTES) return null;\n return text;\n}\n\nwss.on(\"connection\", async (socket, req) => {\n const origin = req?.headers?.origin || \"\";\n if (!httpCors.isWebSocketOriginAllowed(origin)) {\n socket.close(1008, \"Origin not allowed\");\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const path = req?.url || \"\";\n const query = new URL(`ws://localhost${path}`).searchParams;\n const token = query.get(\"token\");\n if (!token || token !== REQUIRED_GATEWAY_TOKEN) {\n socket.close(1008, \"Invalid gateway token\");\n return;\n }\n }\n if (REQUIRE_AUTH) {\n const result = await verifyClientToken(req);\n if (!result.ok) {\n const isLocal = origin.includes(\"localhost\") || origin.includes(\"127.0.0.1\") || !origin;\n if (!(ALLOW_DEV_ANON && isLocal)) {\n socket.close(1008, result.reason);\n return;\n }\n socket.auth = { ok: true, kind: \"dev-anon\", user: { uid: \"dev-anon\" } };\n } else {\n socket.auth = result;\n }\n }\n\n socket.clientId = `c-${Math.random().toString(36).slice(2, 8)}`;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.HELLO, { message: \"Castle Gateway online.\", clientId: socket.clientId })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.WORLD_SNAPSHOT, orchestrator.worldSnapshot())));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.BROADCAST_STATE, { broadcasterClientId, viewers: [...wss.clients].filter((c) => c.readyState === 1).length })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload())));\n broadcastExcept(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }), socket);\n\n socket.on(\"message\", (raw) => {\n const safeRaw = sanitizeRawMessage(raw);\n if (!safeRaw) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Message too large or invalid.\" })));\n return;\n }\n const parsed = safeJsonParse(safeRaw);\n if (!parsed?.type) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid message.\" })));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND_TEXT) {\n const canonical = parseTextToCommand(parsed.payload?.text);\n if (!canonical) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Unknown command text.\" })));\n return;\n }\n const result = orchestrator.applyCommand(canonical);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND) {\n const result = orchestrator.applyCommand(parsed.payload);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n }\n\n if (parsed.type === WS_MESSAGE.INPUT_FRAME) {\n const err = validateInputFrame(parsed.payload, socket);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const frame = parsed.payload.frame;\n const commands = Array.isArray(parsed.payload?.commands) ? parsed.payload.commands : [];\n orchestrator.queueInputFrame(socket.clientId, frame, commands);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, queued: true, frame })));\n }\n\n if (parsed.type === WS_MESSAGE.SIGNAL) {\n const payload = parsed.payload || {};\n if (Object.values(CASTLE_NETWORK_SIGNAL_V0).includes(payload.signalType)) {\n const netErr = validateCastleNetworkSignalPayloadV0(socket, payload);\n if (netErr) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: netErr })));\n return;\n }\n handleCastleNetworkSignalV0(socket, payload, wss);\n return;\n }\n const err = validateSignalPayload(payload);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const signalType = payload.signalType;\n const to = payload.to;\n for (const client of wss.clients) {\n if (client.clientId === to && client.readyState === 1) {\n client.send(JSON.stringify(createEnvelope(WS_MESSAGE.SIGNAL, { ...payload, from: socket.clientId })));\n break;\n }\n }\n }\n\n if (parsed.type === WS_MESSAGE.DRONE_TELEMETRY) {\n const payload = parsed.payload || {};\n const json = JSON.stringify(payload);\n if (Buffer.byteLength(json, \"utf8\") > 48 * 1024) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"DRONE_TELEMETRY payload too large.\" })));\n return;\n }\n broadcastExcept(createEnvelope(WS_MESSAGE.DRONE_TELEMETRY, { ...payload, relayFrom: socket.clientId }), socket);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_START) {\n handleRhizohVoiceLiveStartV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_CHUNK) {\n handleRhizohVoiceLiveChunkV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_STOP) {\n handleRhizohVoiceLiveStopV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_SOCIAL_PULSE) {\n handleCastleSocialPulse(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_WAL_PEER_FEED) {\n handleCastleWalPeerFeed(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.OPEN_DATA_QUERY) {\n queryOpenData(parsed.payload)\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: true, result })));\n })\n .catch((e) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: false, error: e.message })));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_QUERY) {\n runRhizohBrainV2({\n ...(parsed.payload || {}),\n uid: socket?.auth?.user?.uid || \"unknown\"\n })\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, result)));\n })\n .catch(async () => {\n const fallback = await runRhizohBrain(parsed.payload || {}).catch(() => ({\n ok: false,\n reply: \"Rhizoh temporary disturbance. Habitat remains stable.\"\n }));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, fallback)));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.BROADCAST_REGISTER) {\n const role = String(parsed.payload?.role || \"\");\n if (role === \"GENESIS_BROADCAST_AGENT\") {\n broadcasterClientId = socket.clientId;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, registered: role })));\n broadcastState();\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.STUDIO_CUE) {\n // Only registered broadcaster can push global cues.\n if (!broadcasterClientId || socket.clientId !== broadcasterClientId) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Only broadcaster agent can emit STUDIO_CUE.\" })));\n return;\n }\n const cue = parsed.payload || {};\n if (typeof cue !== \"object\") {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid STUDIO_CUE payload.\" })));\n return;\n }\n broadcast(createEnvelope(WS_MESSAGE.STUDIO_CUE, cue));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_JOIN_ROOM) {\n const requested = String(parsed.payload?.roomId || \"\").trim();\n const requestedName = String(parsed.payload?.roomName || \"\").trim();\n const roomId = ensureRoom(requested || `room-${Math.random().toString(36).slice(2, 7)}`, requestedName || null);\n spiralState.activeByClient.set(socket.clientId, roomId);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, roomId })));\n broadcastSpiralState();\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_CREATE_CHARACTER) {\n const name = String(parsed.payload?.name || \"\").trim().slice(0, 32) || `AI-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;\n const role = String(parsed.payload?.role || \"agent\").trim().slice(0, 24) || \"agent\";\n const roomId = ensureRoom(\n String(parsed.payload?.roomId || spiralState.activeByClient.get(socket.clientId) || spiralState.rooms[0].id),\n null\n );\n\n // Character creation always maps to an authoritative world agent.\n const result = orchestrator.applyCommand({\n command: COMMAND.SPAWN_AGENT,\n params: { targetCastleId: \"castle-01\" }\n });\n if (!result?.ok || !result?.data?.id) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Character creation failed.\" })));\n return;\n }\n const id = result.data.id;\n spiralState.characters.set(id, { id, name, role, roomId, source: \"spawned\" });\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, characterId: id, roomId })));\n broadcastSpiralState();\n return;\n }\n });\n\n socket.on(\"close\", () => {\n if (socket.clientId === broadcasterClientId) broadcasterClientId = null;\n clientStats.delete(socket.clientId);\n rhizohVoiceLiveSessions.delete(socket);\n spiralState.activeByClient.delete(socket.clientId);\n removeCastleSocialClientFromAllRooms(socket.clientId);\n removeCastleNetworkClientV0(socket.clientId, wss);\n broadcast(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }));\n broadcastState();\n });\n});\n\nsetInterval(() => {\n void (async () => {\n try {\n const { frame, snapshot, delta, packedDelta } = orchestrator.tick(1 / 20);\n syncSpiralCharactersWithWorld(snapshot);\n broadcast(createEnvelope(WS_MESSAGE.NETWORK_TICK, { frame }));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_TICK, snapshot));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA, delta));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA_PACKED, packedDelta));\n broadcastSpiralState();\n await runAcademyEventTick();\n const executed = await tickAutomations(Date.now());\n for (const a of executed) {\n await addTranscript(a.uid, {\n source: \"planner\",\n eventType: \"automation-executed\",\n text: `Automation executed: ${a.title} (${a.action})`,\n roomId: \"studio-main\",\n meta: { automationId: a.id, action: a.action, deviceId: a.deviceId || \"\" }\n });\n }\n } catch (e) {\n console.error(\"[GATEWAY] world tick / automations loop error:\", String(e?.message || e));\n }\n })();\n}, 50);\n\n(async () => {\n await rhizohRuntime.telemetry.initOpenTelemetry();\n if (String(process.env.CASTLE_GENESIS_DISK_PERSIST ?? \"\").trim() === \"\") {\n process.env.CASTLE_GENESIS_DISK_PERSIST = \"1\";\n }\n const genesisDiskBoot = await hydrateGenesisContinuityPersistenceBootV0();\n if (!genesisDiskBoot.ok) {\n console.warn(\"[GATEWAY] genesis disk hydrate failed; continuing with in-memory genesis continuity\");\n }\n httpServer.listen(PORT, () => {\n startGenesisCanonicalClock();\n installGenesisCheckpointSurfaceGetter(buildGenesisRuntimeSurfacePayloadLive);\n setGenesisContinuityAfterPublishHook((seq) => {\n noteGenesisCheckpointSeqCommitted(seq);\n noteGenesisContinuitySeqAcceptedV0(seq);\n });\n startGenesisContinuityInfraSampler(buildGenesisRuntimeSurfacePayloadLive, 2000);\n logProductionObservatorySurfaceGuardsV0();\n logGatewaySubstrateAuthorityGuardsV0();\n if (isLlmWorkerEnabledV0()) {\n try {\n const workerBoot = initLlmWorkerAtBootV0();\n console.log(\"[GATEWAY] llm worker boot:\", workerBoot);\n } catch (workerErr) {\n console.error(\"[GATEWAY] llm worker boot failed:\", workerErr);\n }\n }\n console.log(`[GATEWAY] ws/http://localhost:${PORT}`);\n console.log(\n `[GATEWAY] genesis continuity: GET ${rhizohRuntime.routes.genesisRuntime} | POST ${rhizohRuntime.routes.genesisIngress} | SSE ${rhizohRuntime.routes.genesisStream} | replay ${rhizohRuntime.routes.genesisReplay} | diff ${rhizohRuntime.routes.genesisReplayDiff} | equiv ${rhizohRuntime.routes.genesisReplayEquivalence} | analytics ${rhizohRuntime.routes.genesisReplayAnalytics} | evolution ${rhizohRuntime.routes.genesisReplayEvolution}`\n );\n if (String(process.env.CASTLE_GENESIS_OBSERVATORY_LIVE_V01 ?? \"\").trim() === \"1\") {\n console.log(\"GENESIS OBSERVATORY LIVE v0.1 — READ ONLY PHASE STARTED\");\n }\n });\n})().catch((e) => {\n console.error(\"[GATEWAY] fatal boot (before or during listen):\", e);\n process.exit(1);\n});\n"}, "bffdb6621cd4af277b867ba4ab2c2a1e41ae22c6": {"t": 1785754128.4180186, "n": 175584, "h": "0b2f1b103f1c9211", "c": "import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport dotenv from \"dotenv\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconsole.log(\"🔥 [GENESIS_BOOT] entry file:\", __filename);\nconsole.log(\"🔥 [GENESIS_BOOT] node env:\", process.env.NODE_ENV);\nconsole.log(\"🔥 [GENESIS_BOOT] pid:\", process.pid);\nconsole.log(\"🧬 [GENESIS_BOOT] cwd:\", process.cwd());\nconsole.log(\"🧬 [GENESIS_BOOT] import.meta.url:\", import.meta.url);\ndotenv.config({ path: path.join(__dirname, \"..\", \".env\") });\ndotenv.config({ path: path.join(__dirname, \"..\", \".env.local\"), override: true });\n\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport { createServer } from \"node:http\";\nimport { createOrchestrator } from \"@castle/orchestrator\";\nimport { parseTextToCommand } from \"@castle/command-dsl\";\nimport {\n WS_MESSAGE,\n COMMAND,\n createEnvelope,\n createRhizohImmutableEventV0,\n createRhizohPayloadRefV0,\n safeJsonParse,\n validateRhizohVoiceLiveChunkPayloadV0\n} from \"@castle/protocol\";\nimport { queryOpenData } from \"./openData.js\";\nimport { verifyClientToken } from \"./auth.js\";\nimport { runRhizohBrain } from \"./rhizohBrain.js\";\nimport { queryRhizohLlm } from \"./rhizohLlmGateway.js\";\nimport { rhizohGatewayTurn } from \"./rhizohGatewayTurn.js\";\nimport {\n enqueueLlmWorkerTaskAsyncV0,\n getLatestLlmWorkerTaskIdV0,\n getLlmWorkerDebugSnapshotV0,\n getLlmWorkerTaskSnapshotV0,\n initLlmWorkerAtBootV0,\n isLlmWorkerAsyncEnabledV0,\n isLlmWorkerEnabledV0,\n runLlmWorkerTaskV0\n} from \"./llmWorkerRuntime.js\";\nimport {\n RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n runRhizohVoiceTranscribeV3,\n rhizohVoiceTranscribeEnvV3\n} from \"./rhizohVoiceTranscribeV3.js\";\nimport { createHttpCorsPolicy, normalizeHttpOrigin, parseAllowedOriginsFromEnv } from \"./httpCorsPolicyV1.js\";\nimport {\n applyLanguagePropagationToLlmPayloadV1,\n applyLanguagePropagationToVoiceBodyV1,\n buildRhizohLanguagePropagationEchoV1,\n parseRhizohLanguagePropagationV1\n} from \"./rhizohLanguagePropagationV1.js\";\nimport { trimRhizohLlmGatewayPayloadV1 } from \"./rhizohLlmPayloadTrimV1.js\";\nimport {\n meshAppendDelta,\n meshContinuityAggregate,\n meshJoin,\n meshLeave,\n meshReplayFromNodeId,\n meshReplayFromSeq,\n meshSnapshot,\n meshSubscribeSse\n} from \"./presenceRoomMesh.js\";\nimport { runRhizohBrainV2 } from \"./rhizohBrainV2.js\";\nimport {\n listConnections,\n createConnection,\n updateConnection,\n deleteConnection,\n setDefaultConnection,\n resolveConnection\n} from \"./llmConnectionsStore.js\";\nimport { getCloudSyncSnapshotV0, mergeCloudSyncSnapshotV0 } from \"./castleCloudSyncStore.js\";\nimport {\n CASTLE_NETWORK_SIGNAL_V0,\n handleCastleNetworkSignalV0,\n listCastleNetworkPresenceV0,\n removeCastleNetworkClientV0,\n validateCastleNetworkSignalPayloadV0\n} from \"./castleNetworkRelayV0.js\";\nimport { checkHttpRateLimit, getHttpClientIp } from \"./castleHttpRateLimit.js\";\nimport {\n submitAbuseReportV0,\n listModerationQueueV0,\n softBlockUserV0,\n removeSoftBlockV0,\n listSoftBlockedUsersV0\n} from \"./ops/moderationMvpV0.js\";\nimport { listRecentAgentSnapshotsV0 } from \"./ops/agentObservabilityV0.js\";\nimport { readAgentContainmentConfigV0 } from \"./ops/agentContainmentV0.js\";\nimport { readCostContainmentConfigV0, getCostContainmentStatsV0 } from \"./ops/costContainmentV0.js\";\nimport { getGlobalCostLedgerSnapshotV0, getGclBackendStatusV0 } from \"./ops/globalCostLedgerV0.js\";\nimport { evaluateEconomicStrategyL3V0 } from \"./ops/economicStrategyEngineL3V0.js\";\nimport { getPhasedRolloutStatsV0, readPhasedRolloutTierV0 } from \"./ops/phasedRolloutV0.js\";\nimport { buildUnifiedStateNarrativeV0 } from \"./ops/unifiedStateNarrativeV0.js\";\nimport { resolveHardeningTenantIdV0 } from \"./ops/narrativeTenantIsolationV0.js\";\nimport {\n classifyStressResponseV0,\n verifyStressTaxonomyCoverageV0,\n verifyStressConflictResolutionV0\n} from \"./ops/stressResponseTaxonomyV0.js\";\nimport { verifyResolutionStabilityEnvelopeV0 } from \"./ops/resolutionStabilityEnvelopeV0.js\";\nimport { logLlmAccess } from \"./castleLlmAudit.js\";\nimport { appendMemory, listMemories, getMemoryContext, getPersonaGoalMemory, setPersonaGoalMemory, autoCompactMemories } from \"./memoryStore.js\";\nimport { getFirebasePersistence } from \"./firebasePersistence.js\";\nimport { registerAgentIdentity, listAgentIdentities, getAgentIdentity, updateAgentIdentity } from \"./agentIdentityStore.js\";\nimport { queueAcademyEvent, listAcademyEvents, resolveAcademyEvent, runAcademyEventTick } from \"./academyEventEngine.js\";\nimport { enqueuePdfJob, getPdfJob } from \"./pdfPipeline.js\";\nimport {\n createPublishSession,\n listPublishSessions,\n updatePublishSession,\n addTranscript,\n listTranscripts,\n searchMetadata,\n registerDevice,\n listDevices,\n updateDevice,\n appendTelemetry,\n listTelemetry,\n createAutomation,\n listAutomations,\n tickAutomations,\n enqueueCommand,\n listCommands,\n updateCommandStatus,\n registerSocialChannel,\n listSocialChannels\n} from \"./studioOpsStore.js\";\nimport { metrics as infraMetrics } from \"./infra/metrics.js\";\nimport { renderRhizohPrometheusMetrics, rhizohEnterpriseMetrics } from \"./infra/rhizohEnterpriseMetrics.js\";\nimport { scoreHealth } from \"./infra/healthScore.js\";\nimport { getTraceById, listTracesBySession } from \"./infra/traceRegistry.js\";\nimport { canonicalEpistemicSealString, hashAndSignEpistemicSeal, EPISTEMIC_SEAL_SCHEMA } from \"./epistemicSeal.js\";\nimport { persistEpistemicLedgerBatch } from \"./epistemicLedgerStore.js\";\nimport { persistEpistemicForecastBatch } from \"./epistemicForecastStore.js\";\nimport {\n persistAuthorityLedgerWitnessBatchV1,\n getAuthorityLedgerWitnessSnapshotV1,\n replayAuthorityLedgerWitnessChainV1\n} from \"./authorityLedgerWitnessStoreV1.js\";\nimport { buildRhizohExternalGroundTruthPayload } from \"./rhizohExternalGroundTruthGateway.js\";\nimport { ingestRhizohExternalLossBatchHttp } from \"./rhizohExternalLossIngestGateway.js\";\nimport {\n ingestRhizohProductOutcomeHttp,\n listRhizohProductOutcomeAggregates,\n verifyRhizohOutcomeHmac,\n verifyRhizohOutcomeSourceToken\n} from \"./rhizohProductOutcomeIngestGateway.js\";\nimport { initRhizoh } from \"./rhizohProductionBootstrap.js\";\nimport { handleAcademicObservatoryExportGetV0 } from \"./rhizoh/academicObservatoryHttpV0.js\";\nimport { handleCouncilAnomalyReasoningPostV0 } from \"./council/rhizohEpistemicCouncilHttpV0.js\";\nimport {\n handleLiveSportsBundleGetV0,\n handleLiveNewsHeadlinesGetV0,\n handleLiveWorldFeedGetV0\n} from \"./rhizoh/liveWorldFeedHttpV0.js\";\nimport {\n buildGenesisRuntimeSurfacePayload,\n recordGenesisEpistemicLedgerPersisted,\n recordGenesisEpistemicSealIssued,\n startGenesisCanonicalClock\n} from \"./genesisRuntimeSurfaceV0.js\";\nimport { registerGenesisSseClient, getGenesisContinuitySeq, setGenesisContinuityAfterPublishHook } from \"./genesisContinuityStreamHubV0.js\";\nimport { computeGenesisReplayFingerprintV0 } from \"./genesisReplayFingerprintV0.js\";\nimport { buildGenesisGatewayCapabilitiesV0 } from \"./genesisGatewayCapabilitiesV0.js\";\nimport {\n installGenesisCheckpointSurfaceGetter,\n noteGenesisCheckpointSeqCommitted,\n getLatestGenesisSignedCheckpoint\n} from \"./genesisContinuityCheckpointV0.js\";\nimport { hydrateGenesisContinuityPersistenceBootV0 } from \"./genesisContinuityHydrateBootV0.js\";\nimport { startGenesisContinuityInfraSampler } from \"./genesisContinuityInfraSamplerV0.js\";\nimport {\n genesisCheckpointQueryBySeqV0,\n genesisCheckpointQueryRangeV0,\n genesisCheckpointQueryLineageV0\n} from \"./genesisCheckpointQueryV0.js\";\nimport {\n genesisContinuityEventArchiveEnabled,\n queryGenesisContinuityEventArchiveV0,\n GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION\n} from \"./genesisContinuityEventArchiveV0.js\";\nimport { resolveGenesisReplayRouterV1, GENESIS_REPLAY_ROUTER_SCHEMA } from \"./genesisReplayRouterV1.js\";\nimport {\n computeGenesisReplayTemporalDiffV1,\n GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA\n} from \"./genesisReplayTemporalDiffV1.js\";\nimport { compareGenesisReplayEquivalenceV1 } from \"./genesisReplayEquivalenceV1.js\";\nimport { computeGenesisReplayAnalyticsV1 } from \"./genesisReplayAnalyticsV1.js\";\nimport { computeGenesisReplayEvolutionV1 } from \"./genesisReplayEvolutionV1.js\";\nimport { handleStripeCheckoutCreate, handleStripeMembershipWebhook } from \"./stripeWebhookMembership.js\";\nimport { buildRealityHealthPayload } from \"./edgeRealityConsistencyV1.js\";\nimport {\n assertWalPeerSocketAuthorityV0,\n buildGatewaySubstrateAuthoritySnapshotV0,\n logGatewaySubstrateAuthorityGuardsV0,\n validateWalPeerFeedSignatureV0\n} from \"./gatewaySubstrateAuthorityV0.js\";\nimport {\n buildSubstrateOperationalSnapshotV0,\n recordWalPeerRejectV0,\n renderSubstratePrometheusMetricsV0\n} from \"./infra/substrateOperationalMetrics.js\";\nimport { ingestClientSubstrateHealthV0 } from \"./rhizohSubstrateHealthIngestV0.js\";\nimport { recordSubstrateOtelEventV0 } from \"./infra/opentelemetrySubstrateV0.js\";\nimport {\n GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n getGenesisWorldObservationIngressStatusV0,\n ingestGenesisWorldObservationV0\n} from \"./genesisContinuityClientIngressV0.js\";\nimport {\n getGenesisContinuitySeqAuditV0,\n noteGenesisContinuitySeqAcceptedV0\n} from \"./genesisContinuitySeqGapV0.js\";\n\n/** Render / Fly / Railway: `PORT` — yoksa `CASTLE_GATEWAY_PORT` — yoksa 8090. */\nconst PORT =\n Number(process.env.PORT) ||\n Number(process.env.CASTLE_GATEWAY_PORT) ||\n 8090;\n/** Spiral / world WebSocket server — set after `wss` init; used by genesis runtime surface. */\nlet spiralWssForGenesis = null;\nconst MAX_MESSAGE_BYTES = Number(process.env.CASTLE_MAX_MESSAGE_BYTES || 32 * 1024);\nconst REQUIRED_GATEWAY_TOKEN = process.env.CASTLE_GATEWAY_TOKEN || \"\";\nconst ALLOWED_ORIGINS = parseAllowedOriginsFromEnv().fromList;\nconst httpCors = createHttpCorsPolicy();\n\n/** @returns {string|null} `Access-Control-Allow-Origin`; null = browser CORS fail. */\nfunction accessControlAllowOriginValue(req) {\n return httpCors.accessControlAllowOriginValue(req);\n}\n\nfunction applyHttpCorsHeaders(req, res) {\n return httpCors.applyHttpCorsHeaders(req, res);\n}\nconst REQUIRE_AUTH = process.env.CASTLE_REQUIRE_AUTH === \"true\";\nconst ALLOW_DEV_ANON = process.env.CASTLE_ALLOW_DEV_ANON !== \"false\";\nconst ALLOW_DEV_HTTP_UID = process.env.CASTLE_ALLOW_DEV_HTTP_UID !== \"false\";\n/** Presence mesh: allow `X-Castle-Guest-Id` when Firebase auth absent (default on). Set `false` to require auth only. */\nconst ALLOW_MESH_GUEST = process.env.CASTLE_ALLOW_MESH_GUEST !== \"false\";\nconst SOCIAL_RETRY_MAX = Math.max(0, Math.min(5, Number(process.env.CASTLE_SOCIAL_RETRY_MAX || 2)));\nconst SOCIAL_RETRY_BASE_MS = Math.max(100, Number(process.env.CASTLE_SOCIAL_RETRY_BASE_MS || 700));\nconst TELEGRAM_BOT_TOKEN = process.env.CASTLE_TELEGRAM_BOT_TOKEN || \"\";\nconst WHATSAPP_TOKEN = process.env.CASTLE_WHATSAPP_TOKEN || \"\";\nconst WHATSAPP_PHONE_NUMBER_ID = process.env.CASTLE_WHATSAPP_PHONE_NUMBER_ID || \"\";\nconst RL_RHIZOH_LLM_PER_MIN = Math.max(5, Number(process.env.CASTLE_RL_RHIZOH_LLM_PER_MIN || 40));\nconst RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN || 30)\n);\nconst RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN || 120));\nconst RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN = Math.max(\n 10,\n Number(process.env.CASTLE_RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN || 60)\n);\nconst RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN = Math.max(8, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN || 48));\nconst RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN = Math.max(4, Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN || 24));\nconst RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN || 18)\n);\nconst RL_EPISTEMIC_SEAL_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_EPISTEMIC_SEAL_PER_MIN || 120));\nconst RL_EPISTEMIC_LOG_PER_MIN = Math.max(20, Number(process.env.CASTLE_RL_EPISTEMIC_LOG_PER_MIN || 240));\nconst EPISTEMIC_SEAL_SECRET = String(\n process.env.CASTLE_EPISTEMIC_SEAL_SECRET || process.env.CASTLE_GATEWAY_TOKEN || \"\"\n).trim();\nconst RL_LLM_CONN_TEST_PER_MIN = Math.max(2, Number(process.env.CASTLE_RL_LLM_CONNECTION_TEST_PER_MIN || 8));\n\n/** One-shot production warnings: CORS surface, auth toggle, dev bypass flags (observatory deploy hygiene). */\nfunction logProductionObservatorySurfaceGuardsV0() {\n if (String(process.env.NODE_ENV || \"\").toLowerCase() !== \"production\") return;\n const tag = \"[GATEWAY_PROD_GUARD]\";\n const primaryCors = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n const corsListEmpty = ALLOWED_ORIGINS.length === 0;\n if (corsListEmpty && !primaryCors) {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty and CASTLE_HTTP_CORS_ORIGIN empty — production still merges RHIZOH_BROWSER_ORIGINS_V1 defaults; set env for preview/staging hosts`\n );\n } else if (corsListEmpty && primaryCors && primaryCors !== \"*\") {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty — only CASTLE_HTTP_CORS_ORIGIN set; add web.app + firebaseapp.com (and previews) to CASTLE_ALLOWED_ORIGINS if clients use alternate hostnames`\n );\n }\n if (primaryCors && /localhost|127\\.0\\.0\\.1/i.test(primaryCors)) {\n console.warn(`${tag} CORS: CASTLE_HTTP_CORS_ORIGIN is localhost while NODE_ENV=production`);\n }\n if (REQUIRE_AUTH) {\n console.warn(\n `${tag} AUTH: CASTLE_REQUIRE_AUTH=true — clients must send a valid gateway credential (otherwise silent 401 on replay/SSE); EventSource cannot set custom headers unless proxied`\n );\n if (REQUIRED_GATEWAY_TOKEN.length < 16) {\n console.warn(`${tag} AUTH: CASTLE_GATEWAY_TOKEN missing or short while require-auth enabled`);\n }\n }\n if (ALLOW_DEV_ANON) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_ANON is not \"false\" — dev anonymous path may be live in production (set CASTLE_ALLOW_DEV_ANON=false)`\n );\n }\n if (ALLOW_DEV_HTTP_UID) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_HTTP_UID is not \"false\" — X-Castle-Dev-Uid bypass risk (set CASTLE_ALLOW_DEV_HTTP_UID=false)`\n );\n }\n}\n\nconst rhizohRuntime = initRhizoh();\n\n/** İstemci: llmKeySource veya keySource — env | user_connection | auto */\nfunction normalizeClientLlmKeySource(raw) {\n const s = String(raw ?? \"\").trim().toLowerCase();\n if (s === \"env\" || s === \"server\") return \"env\";\n if (s === \"user_connection\" || s === \"connection\" || s === \"user\") return \"user_connection\";\n if (s === \"auto\" || s === \"\") return \"auto\";\n return null;\n}\n\n/**\n * Server-side provider wins for env/auto unless user_connection — client bundle may still send provider=openai.\n * @param {\"env\"|\"user_connection\"|\"auto\"} keyMode\n * @param {{ provider?: string, model?: string, apiKey?: string } | null} conn\n * @param {{ provider?: string, model?: string }} payload\n */\nfunction resolveServerLlmProviderV0(keyMode, conn, payload) {\n const envProvider = String(process.env.CASTLE_LLM_PROVIDER || \"\").trim() || undefined;\n const envModel = String(process.env.CASTLE_LLM_MODEL || \"\").trim() || undefined;\n\n if (keyMode === \"user_connection\") {\n return {\n resolvedProvider: conn?.provider || payload?.provider,\n resolvedModel: conn?.model || payload?.model,\n connApiKey: conn?.apiKey || \"\"\n };\n }\n\n if (keyMode === \"env\") {\n return {\n resolvedProvider: envProvider ?? payload?.provider,\n resolvedModel: envModel ?? payload?.model,\n connApiKey: \"\"\n };\n }\n\n if (conn?.provider && conn?.apiKey) {\n return {\n resolvedProvider: conn.provider,\n resolvedModel: conn.model,\n connApiKey: conn.apiKey\n };\n }\n\n return {\n resolvedProvider: envProvider ?? payload?.provider,\n resolvedModel: envModel ?? payload?.model,\n connApiKey: \"\"\n };\n}\n\nfunction jsonReplacerSafeV0(_key, value) {\n if (typeof value === \"bigint\") return Number(value);\n return value;\n}\n\nfunction sendJson(res, statusCode, payload) {\n res.writeHead(statusCode, { \"Content-Type\": \"application/json; charset=utf-8\" });\n try {\n res.end(JSON.stringify(payload, jsonReplacerSafeV0));\n } catch (error) {\n res.end(\n JSON.stringify({\n ok: false,\n error: \"response_serialize_failed\",\n detail: String(error?.message || error)\n })\n );\n }\n}\n\nfunction logRhizohHealth(stage, detail = {}) {\n try {\n console.info(`[CASTLE_${String(stage || \"unknown\")}]`, detail && typeof detail === \"object\" ? detail : {});\n } catch {\n /* noop */\n }\n}\n\nfunction renderInfraMetricsProm() {\n const lines = [\n `castle_gateway_events_processed_total ${infraMetrics.eventsProcessed}`,\n `castle_gateway_duplicate_reject_total ${infraMetrics.duplicateRejects}`,\n `castle_gateway_enqueue_latency_ms ${infraMetrics.enqueueLatencyMs}`,\n `castle_gateway_queue_depth ${infraMetrics.queueDepth}`,\n `castle_gateway_queue_lag_seconds ${(Number(infraMetrics.queueLag || 0) / 1000).toFixed(3)}`,\n `castle_gateway_errors_total ${infraMetrics.errors}`\n ];\n const buckets = infraMetrics.enqueueLatencyBuckets || {};\n for (const k of Object.keys(buckets)) {\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"${k}\"} ${buckets[k]}`);\n }\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"+Inf\"} ${infraMetrics.eventsProcessed}`);\n if (process.env.CASTLE_SUBSTRATE_METRICS !== \"0\") {\n lines.push(renderSubstratePrometheusMetricsV0().trimEnd());\n }\n return lines.join(\"\\n\");\n}\n\nasync function fetchJsonWithTimeout(url, timeoutMs = 1200) {\n const ctl = new AbortController();\n const tid = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const res = await fetch(url, { signal: ctl.signal });\n if (!res.ok) return null;\n return await res.json();\n } catch {\n return null;\n } finally {\n clearTimeout(tid);\n }\n}\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction getHttpPathname(req) {\n let p = String(req.url || \"\").split(\"?\")[0];\n /* Reverse proxies / clients sometimes append `/` — avoid silent 404 on e.g. `/rhizoh/genesis/stream/`. */\n while (p.length > 1 && p.endsWith(\"/\")) {\n p = p.slice(0, -1);\n }\n return p;\n}\n\nfunction rhizohLlmEnvConfigured() {\n const p = String(process.env.CASTLE_LLM_PROVIDER || \"openai\").toLowerCase();\n const key =\n p === \"anthropic\"\n ? process.env.ANTHROPIC_API_KEY\n : p === \"gemini\"\n ? process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY\n : p === \"xai\"\n ? process.env.XAI_API_KEY\n : p === \"deepseek\"\n ? process.env.DEEPSEEK_API_KEY\n : p === \"mistral\"\n ? process.env.MISTRAL_API_KEY\n : p === \"openrouter\"\n ? process.env.OPENROUTER_API_KEY\n : process.env.OPENAI_API_KEY;\n return !!String(key || \"\").trim();\n}\n\n/**\n * @param {import(\"firebase-admin/firestore\").Firestore | null} db\n * @param {number} timeoutMs\n */\nasync function probeFirestoreReachable(db, timeoutMs = 2000) {\n if (!db) return { ok: false, reason: \"no_db\" };\n const timeout = sleep(timeoutMs).then(() => ({ ok: false, reason: \"timeout\" }));\n const work = db\n .collection(\"_castle_gateway_health\")\n .doc(\"ping\")\n .get()\n .then(() => ({ ok: true, reason: \"reachable\" }))\n .catch((e) => ({ ok: false, reason: String(e?.message || e || \"firestore_error\") }));\n return Promise.race([work, timeout]);\n}\n\nfunction stripHtml(text = \"\") {\n return String(text)\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \" \")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \" \")\n .replace(/<[^>]+>/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction haversineMeters(aLat, aLon, bLat, bLon) {\n const R = 6371000;\n const dLat = ((bLat - aLat) * Math.PI) / 180;\n const dLon = ((bLon - aLon) * Math.PI) / 180;\n const sa = Math.sin(dLat / 2);\n const sb = Math.sin(dLon / 2);\n const x = sa * sa + Math.cos((aLat * Math.PI) / 180) * Math.cos((bLat * Math.PI) / 180) * sb * sb;\n return 2 * R * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));\n}\n\nasync function checkPublishTargetHealth(protocol, target) {\n const p = String(protocol || \"\").toUpperCase();\n const t = String(target || \"\").trim();\n if (!t) return { ok: false, reason: \"target_missing\", shouldReconnect: false };\n if (p === \"RTMP\") {\n const ok = t.startsWith(\"rtmp://\") || t.startsWith(\"rtmps://\");\n return { ok, reason: ok ? \"rtmp_target_format_ok\" : \"invalid_rtmp_target\", shouldReconnect: !ok, reconnectInSec: 15 };\n }\n if (p === \"WHIP\" || p === \"SFU\") {\n try {\n const res = await fetch(t, { method: \"OPTIONS\" });\n const ok = res.status < 500;\n return { ok, reason: `http_${res.status}`, shouldReconnect: !ok, reconnectInSec: 8 };\n } catch {\n return { ok: false, reason: \"network_unreachable\", shouldReconnect: true, reconnectInSec: 8 };\n }\n }\n return { ok: true, reason: \"unchecked_protocol\", shouldReconnect: false };\n}\n\nasync function fetchTextFromUrl(url) {\n const res = await fetch(url, { method: \"GET\" });\n if (!res.ok) throw new Error(`fetch_${res.status}`);\n const ct = String(res.headers.get(\"content-type\") || \"\");\n if (ct.includes(\"application/pdf\")) throw new Error(\"pdf_binary_not_supported_directly\");\n const body = await res.text();\n return stripHtml(body).slice(0, 14000);\n}\n\nfunction buildTaskActionGraph(task = \"\", context = {}) {\n const t = String(task || \"\").toLowerCase();\n if (t.includes(\"dinner\") || t.includes(\"akşam yemeği\") || t.includes(\"aksam yemegi\")) {\n return {\n taskTitle: \"Dinner Prep Planner\",\n nodes: [\n { id: \"n1\", title: \"Kitchen preheat\", action: \"smart-oven.preheat\", params: { temperatureC: 190, mode: \"fan\" } },\n { id: \"n2\", title: \"Lighting comfort\", action: \"smart-light.set_scene\", params: { scene: \"warm_dinner\" } },\n { id: \"n3\", title: \"Fridge inventory check\", action: \"smart-fridge.inventory_scan\", params: {} },\n { id: \"n4\", title: \"Reminder notification\", action: \"smart-watch.notify\", params: { text: \"Dinner prep started.\" } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" },\n { from: \"n3\", to: \"n4\" }\n ]\n };\n }\n return {\n taskTitle: task || \"General Planner\",\n nodes: [\n { id: \"n1\", title: \"Safety check\", action: \"device.safety_check\", params: { context } },\n { id: \"n2\", title: \"Primary action\", action: \"device.execute_task\", params: { task } },\n { id: \"n3\", title: \"Report\", action: \"studio.timeline_note\", params: { task } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" }\n ]\n };\n}\n\nfunction buildEthicsPrograms() {\n return {\n policyVersion: \"v1\",\n tracks: [\n {\n id: \"elderly-care-assist\",\n audience: \"elderly\",\n title: \"Elderly Assistance Track\",\n principles: [\"human-in-the-loop\", \"consent-first\", \"low-friction alerts\"],\n modules: [\"medication-reminder\", \"fall-risk-check\", \"family-notify\", \"daily-companion-brief\"]\n },\n {\n id: \"kids-safe-learning\",\n audience: \"children\",\n title: \"Kids Safe Learning Track\",\n principles: [\"guardian-consent\", \"age-appropriate-language\", \"content-filtering\"],\n modules: [\"study-coach\", \"screen-time-balance\", \"safe-social-share\", \"emergency-contact\"]\n }\n ]\n };\n}\n\nasync function deliverViaTelegram(channel, text) {\n const chatId = String(channel.endpoint || \"\").trim();\n if (!TELEGRAM_BOT_TOKEN) throw new Error(\"telegram_token_missing\");\n if (!chatId) throw new Error(\"telegram_chat_id_missing\");\n const url = `https://api.telegram.org/bot${encodeURIComponent(TELEGRAM_BOT_TOKEN)}/sendMessage`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ chat_id: chatId, text })\n });\n if (!res.ok) throw new Error(`telegram_http_${res.status}`);\n return { provider: \"telegram\", status: res.status };\n}\n\nasync function deliverViaWhatsapp(channel, text) {\n const to = String(channel.endpoint || \"\").trim();\n if (!WHATSAPP_TOKEN) throw new Error(\"whatsapp_token_missing\");\n if (!WHATSAPP_PHONE_NUMBER_ID) throw new Error(\"whatsapp_phone_number_id_missing\");\n if (!to) throw new Error(\"whatsapp_to_missing\");\n const url = `https://graph.facebook.com/v22.0/${encodeURIComponent(WHATSAPP_PHONE_NUMBER_ID)}/messages`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${WHATSAPP_TOKEN}` },\n body: JSON.stringify({ messaging_product: \"whatsapp\", to, type: \"text\", text: { body: text } })\n });\n if (!res.ok) throw new Error(`whatsapp_http_${res.status}`);\n return { provider: \"whatsapp\", status: res.status };\n}\n\nasync function deliverViaWebhook(channel, text) {\n const endpoint = String(channel.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"webhook_endpoint_missing\");\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text, platform: channel.platform || \"webhook\", ts: Date.now() })\n });\n if (!res.ok) throw new Error(`webhook_http_${res.status}`);\n return { provider: \"webhook\", status: res.status };\n}\n\nasync function deliverSocialMessage(channel, text) {\n const platform = String(channel.platform || \"webhook\").toLowerCase();\n if (platform === \"telegram\") return deliverViaTelegram(channel, text);\n if (platform === \"whatsapp\") return deliverViaWhatsapp(channel, text);\n return deliverViaWebhook(channel, text);\n}\n\nasync function deliverSocialWithRetry(channel, text) {\n let attempt = 0;\n let lastError = null;\n while (attempt <= SOCIAL_RETRY_MAX) {\n try {\n const result = await deliverSocialMessage(channel, text);\n return { ok: true, attempt: attempt + 1, ...result };\n } catch (error) {\n lastError = error;\n if (attempt >= SOCIAL_RETRY_MAX) break;\n const jitter = Math.floor(Math.random() * 90);\n await sleep(SOCIAL_RETRY_BASE_MS * Math.pow(2, attempt) + jitter);\n }\n attempt += 1;\n }\n return { ok: false, attempt: attempt + 1, error: String(lastError?.message || \"delivery_failed\") };\n}\n\nasync function dispatchCommandToAdapter(device, command) {\n const adapter = String(command?.adapter || device?.adapter || \"websocket\").toLowerCase();\n const endpoint = String(device?.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"device_endpoint_missing\");\n if (adapter === \"mqtt\") {\n // MQTT delivery is proxied via a bridge endpoint (HTTP webhook style).\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n type: \"mqtt-command\",\n topic: `castle/devices/${device.id}/commands`,\n action: command.action,\n params: command.params || {}\n })\n });\n if (!res.ok) throw new Error(`mqtt_bridge_http_${res.status}`);\n return { ok: true, adapter, via: \"http-bridge\", status: res.status };\n }\n if (!endpoint.startsWith(\"ws://\") && !endpoint.startsWith(\"wss://\")) throw new Error(\"invalid_websocket_endpoint\");\n const sendResult = await new Promise((resolve, reject) => {\n const ws = new WebSocket(endpoint);\n const timer = setTimeout(() => {\n try {\n ws.close();\n } catch {\n /* noop */\n }\n reject(new Error(\"websocket_timeout\"));\n }, 3000);\n ws.on(\"open\", () => {\n ws.send(\n JSON.stringify({\n type: \"castle-command\",\n commandId: command.id,\n deviceId: command.deviceId,\n action: command.action,\n params: command.params || {}\n }),\n (err) => {\n clearTimeout(timer);\n ws.close();\n if (err) reject(new Error(\"websocket_send_failed\"));\n else resolve({ ok: true, adapter: \"websocket\", via: \"ws\" });\n }\n );\n });\n ws.on(\"error\", () => {\n clearTimeout(timer);\n reject(new Error(\"websocket_connect_failed\"));\n });\n });\n return sendResult;\n}\n\nfunction readHttpJson(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => {\n if (!buf) return resolve({});\n const payload = safeJsonParse(buf);\n if (!payload || typeof payload !== \"object\") return reject(new Error(\"invalid_json_payload\"));\n resolve(payload);\n });\n req.on(\"error\", reject);\n });\n}\n\n/** Ham gövde (HMAC doğrulaması için). */\nfunction readHttpBodyText(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => resolve(buf));\n req.on(\"error\", reject);\n });\n}\n\nfunction maskOpaqueId(value) {\n const t = String(value || \"\");\n if (!t) return \"(empty)\";\n if (t.length <= 8) return \"****\";\n return `${t.slice(0, 4)}…${t.slice(-2)}`;\n}\n\nfunction readGatewayHttpToken(req) {\n const x = String(req.headers?.[\"x-castle-gateway-token\"] || \"\").trim();\n if (x) return x;\n const auth = String(req.headers?.authorization || \"\");\n const m = /^Bearer\\s+(.+)$/i.exec(auth);\n return m ? String(m[1] || \"\").trim() : \"\";\n}\n\nasync function resolveHttpUser(req) {\n const authResult = await verifyClientToken(req);\n if (authResult?.ok && authResult.user?.uid) {\n return { ok: true, uid: String(authResult.user.uid), mode: authResult.kind || \"auth\" };\n }\n if (ALLOW_DEV_ANON && ALLOW_DEV_HTTP_UID) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) return { ok: true, uid: `dev-${devUid.slice(0, 64)}`, mode: \"dev-header\" };\n }\n return { ok: false, reason: authResult?.reason || \"auth_required\" };\n}\n\n/**\n * Epistemic ledger ingest — Firebase uid OR valid transport token + X-Castle-Dev-Uid.\n * Prod disables CASTLE_ALLOW_DEV_HTTP_UID; LLM may still work while telemetry 401'd without this.\n */\nasync function resolveEpistemicIngestActor(req) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok && tok === REQUIRED_GATEWAY_TOKEN) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) {\n return { ok: true, uid: `gw-telemetry-${devUid.slice(0, 64)}`, mode: \"gateway-token-dev-uid\" };\n }\n return { ok: false, reason: \"dev_uid_required\" };\n }\n if (tok) {\n return { ok: false, reason: \"gateway_token_mismatch\" };\n }\n }\n const auth = await resolveHttpUser(req);\n if (auth.ok) return auth;\n return { ok: false, reason: auth.reason || \"auth_required\" };\n}\n\n/** Mesh actor: Firebase uid, dev header, or `guest:<uuid>` via header / `guestId` query (SSE cannot set headers). */\nasync function resolveMeshActor(req) {\n const auth = await resolveHttpUser(req);\n if (auth.ok) return { ok: true, clientUid: auth.uid, mode: auth.mode };\n if (ALLOW_MESH_GUEST) {\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const gid = String(req.headers?.[\"x-castle-guest-id\"] || q.searchParams.get(\"guestId\") || \"\").trim();\n if (gid) {\n const normalized = gid.startsWith(\"guest:\") ? gid.slice(0, 180) : `guest:${gid.slice(0, 160)}`;\n return { ok: true, clientUid: normalized, mode: \"guest-header\" };\n }\n }\n return { ok: false, reason: \"mesh_auth_required\" };\n}\n\n/** Live genesis runtime JSON (GET /rhizoh/genesis/runtime + SSE infra sampler). */\nasync function buildGenesisRuntimeSurfacePayloadLive() {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n let spiralWsActive = 0;\n if (spiralWssForGenesis) {\n for (const c of spiralWssForGenesis.clients) {\n if (c.readyState === 1) spiralWsActive += 1;\n }\n }\n const base = buildGenesisRuntimeSurfacePayload({\n infraMetrics,\n scoredHealth: scored,\n rhizohEnterpriseMetrics,\n mesh: meshContinuityAggregate(),\n workerHealth: worker,\n port: PORT,\n spiralWebSocketClientsActive: spiralWsActive\n });\n return {\n ...base,\n replayFingerprint: computeGenesisReplayFingerprintV0(base),\n gatewayCapabilities: buildGenesisGatewayCapabilitiesV0(),\n genesisStream: {\n schema: \"castle.genesis.stream_cursor.v0\",\n lastAcceptedSeq: getGenesisContinuitySeq(),\n clientIngress: {\n schema: GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n contract: \"castle.world_observation.ingress_envelope.v1\",\n postPath: rhizohRuntime.routes.genesisIngress,\n closure: getGenesisWorldObservationIngressStatusV0()\n },\n seqContinuity: getGenesisContinuitySeqAuditV0(),\n eventArchive: genesisContinuityEventArchiveEnabled()\n ? {\n enabled: true,\n projection: GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION,\n queryPath: rhizohRuntime.routes.genesisContinuityEvents\n }\n : { enabled: false },\n replayRouter: {\n schema: GENESIS_REPLAY_ROUTER_SCHEMA,\n queryPath: rhizohRuntime.routes.genesisReplay,\n queryHint: \"from=&to= or range=from-to; optional type=&checkpoints=0\",\n temporalDiffPath: rhizohRuntime.routes.genesisReplayDiff,\n temporalDiffSchema: GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA,\n equivalencePath: rhizohRuntime.routes.genesisReplayEquivalence,\n analyticsPath: rhizohRuntime.routes.genesisReplayAnalytics,\n evolutionPath: rhizohRuntime.routes.genesisReplayEvolution\n }\n }\n };\n}\n\nconst httpServer = createServer(async (req, res) => {\n const corsAllow = applyHttpCorsHeaders(req, res);\n if (req.method === \"OPTIONS\") {\n if (!corsAllow && req.headers?.origin) {\n console.warn(\"[GATEWAY_CORS] preflight rejected for origin:\", req.headers.origin);\n }\n res.writeHead(corsAllow ? 204 : 403);\n res.end();\n return;\n }\n\n const pathname = getHttpPathname(req);\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/genesis/__ping\") {\n sendJson(res, 200, {\n ok: true,\n pid: process.pid,\n entry: __filename,\n time: Date.now(),\n genesis: true\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/epistemic/__probe\") {\n sendJson(res, 200, {\n ok: true,\n epistemicIngestActorV2: true,\n routes: {\n seal: rhizohRuntime.routes.epistemicSeal,\n logsBatch: rhizohRuntime.routes.epistemicLogsBatch,\n authorityLedgerBatch: rhizohRuntime.routes.authorityLedgerBatch,\n authorityLedgerReplay: rhizohRuntime.routes.authorityLedgerReplay\n },\n hasGatewayToken: Boolean(REQUIRED_GATEWAY_TOKEN),\n gatewayTokenLen: REQUIRED_GATEWAY_TOKEN ? REQUIRED_GATEWAY_TOKEN.length : 0\n });\n return;\n }\n\n if (\n req.method === \"GET\" &&\n (pathname === \"/rhizoh/live/world-feed\" ||\n pathname === \"/rhizoh/live/sports-bundle\" ||\n pathname === \"/rhizoh/live/news-headlines\")\n ) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_live_world_feed:${ip}`, RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const out =\n pathname === \"/rhizoh/live/sports-bundle\"\n ? await handleLiveSportsBundleGetV0()\n : pathname === \"/rhizoh/live/news-headlines\"\n ? await handleLiveNewsHeadlinesGetV0(req)\n : await handleLiveWorldFeedGetV0(req);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/webhooks/stripe\") {\n await handleStripeMembershipWebhook(req, res);\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/billing/stripe/checkout\") {\n await handleStripeCheckoutCreate(req, res, { readHttpJson, resolveHttpUser, sendJson });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/live\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, live: false, dns: true, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n sendJson(res, 200, { ok: true, live: true, dns: true, service: \"castle-gateway\", ts: Date.now() });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/cors\") {\n const reqOrigin = normalizeHttpOrigin(req.headers?.origin);\n const allowOrigin = accessControlAllowOriginValue(req);\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n requestOrigin: reqOrigin || null,\n allowed: Boolean(allowOrigin),\n allowOrigin: allowOrigin || null,\n allowSetSize: httpCors.allowSet.size\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/ready\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, ready: false, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n try {\n const persistence = getFirebasePersistence();\n const llmProbe = rhizohLlmEnvConfigured();\n sendJson(res, 200, {\n ok: true,\n ready: true,\n dns: true,\n persistence: persistence.mode,\n service: \"castle-gateway\",\n gateway: \"ok\",\n llm: llmProbe ? \"ok\" : \"degraded\",\n spine: llmProbe ? \"ok\" : \"partial\"\n });\n } catch (e) {\n sendJson(res, 503, { ok: false, ready: false, reason: String(e?.message || e), service: \"castle-gateway\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/reality\") {\n const payload = buildRealityHealthPayload();\n sendJson(res, payload.ok ? 200 : 503, payload);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/substrate\") {\n const authority = buildGatewaySubstrateAuthoritySnapshotV0();\n const operational = buildSubstrateOperationalSnapshotV0();\n const drift = buildRealityHealthPayload();\n const ok = Boolean(authority.productionAuthoritySatisfied) && drift.ok;\n sendJson(res, ok ? 200 : 503, {\n ok,\n service: \"castle-gateway\",\n ts: Date.now(),\n authority,\n operational,\n drift: { ok: drift.ok, errors: drift.drift?.errors, warnings: drift.drift?.warnings }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.councilAnomalyReasoning) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_council_anomaly:${ip}`, 30, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const body = await readHttpJson(req);\n const out = handleCouncilAnomalyReasoningPostV0(body);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/substrate/health\") {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const body = await readHttpJson(req);\n const uid = (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestClientSubstrateHealthV0(body, { uid, clientId: uid });\n if (!result.ok) {\n sendJson(res, 400, result);\n return;\n }\n recordSubstrateOtelEventV0(\"client_health_ingest\", { uid: String(uid).slice(0, 32) });\n sendJson(res, 200, result);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/deps\") {\n const t0 = Date.now();\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, {\n ok: false,\n dns: true,\n llm: false,\n firestore: false,\n phase: \"maintenance\",\n latencyMs: Date.now() - t0,\n service: \"castle-gateway\"\n });\n return;\n }\n const persistence = getFirebasePersistence();\n let firestoreOk = persistence.mode !== \"firebase\";\n let firestoreDetail = persistence.mode === \"file\" ? \"file_mode\" : \"not_applicable\";\n if (persistence.mode === \"firebase\" && persistence.db) {\n const pr = await probeFirestoreReachable(persistence.db, 2000);\n firestoreOk = pr.ok;\n firestoreDetail = pr.reason || (pr.ok ? \"reachable\" : \"fail\");\n } else if (persistence.mode === \"firebase\" && !persistence.db) {\n firestoreOk = false;\n firestoreDetail = \"firebase_not_initialized\";\n }\n const llmOk = rhizohLlmEnvConfigured();\n const overallOk = llmOk && firestoreOk;\n const latencyMs = Date.now() - t0;\n sendJson(res, 200, {\n ok: overallOk,\n dns: true,\n llm: llmOk,\n firestore: persistence.mode === \"firebase\" ? firestoreOk : false,\n persistence: persistence.mode,\n firestoreDetail,\n latencyMs,\n service: \"castle-gateway\",\n wsPort: PORT\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health\") {\n const persistence = getFirebasePersistence();\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n wsPort: PORT,\n persistence: persistence.mode,\n presenceMesh: \"sse\"\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/health\") {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n sendJson(res, 200, {\n ok: true,\n role: \"gateway\",\n queueLagMs: infraMetrics.queueLag,\n queueDepth: infraMetrics.queueDepth,\n errors: infraMetrics.errors,\n status: scored.status,\n score: scored.score,\n reasons: scored.reasons,\n worker: worker\n ? {\n status: worker.status,\n score: worker.score,\n reasons: worker.reasons\n }\n : null,\n timestamp: Date.now()\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisRuntime) {\n const payload = await buildGenesisRuntimeSurfacePayloadLive();\n sendJson(res, 200, payload);\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.genesisIngress) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const actor = await resolveMeshActor(req);\n const clientId = actor.ok ? actor.clientUid : (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestGenesisWorldObservationV0(body, { clientId });\n if (!result.ok) {\n const status =\n result.error === \"rate_limited\" || result.error === \"agent_spoke_throttled\"\n ? 429\n : result.error === \"payload_too_large\"\n ? 413\n : 400;\n sendJson(res, status, result);\n return;\n }\n sendJson(res, result.idempotent ? 200 : 202, result);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisStream) {\n const corsOrigin = accessControlAllowOriginValue(req);\n if (!corsOrigin && String(process.env.NODE_ENV || \"\").toLowerCase() === \"production\") {\n const og = String(req.headers?.origin || \"\").trim();\n if (og) {\n console.warn(\"[GATEWAY] genesis SSE: missing Access-Control-Allow-Origin for browser Origin (CORS whitelist mismatch):\", og);\n }\n }\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n res.write(\"retry: 3000\\n\\n\");\n res.write(\": genesis continuity stream\\n\\n\");\n res.write(\"event: genesis\\n\");\n res.write(`data: {\"booted\":true,\"pid\":${process.pid}}\\n\\n`);\n registerGenesisSseClient(res, req);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLatest) {\n const cp = getLatestGenesisSignedCheckpoint();\n if (!cp) {\n sendJson(res, 404, { ok: false, error: \"no_checkpoint_yet\", hint: \"wait_for_seq_interval_or_configure_signing_secret\" });\n return;\n }\n sendJson(res, 200, { ok: true, checkpoint: cp });\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(rhizohRuntime.routes.genesisCheckpointBySeqPrefix)) {\n const tail = pathname.slice(rhizohRuntime.routes.genesisCheckpointBySeqPrefix.length).split(\"/\")[0];\n const seq = parseInt(String(tail || \"\").trim(), 10);\n const out = await genesisCheckpointQueryBySeqV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"checkpoint_not_found\") {\n sendJson(res, 404, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointRange) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryRangeV0(from, to);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"range_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLineage) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const seq = parseInt(String(u.searchParams.get(\"seq\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryLineageV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"lineage_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisContinuityEvents) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const limit = parseInt(String(u.searchParams.get(\"limit\") || \"\").trim(), 10);\n const out = await queryGenesisContinuityEventArchiveV0(from, to, type, limit);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\" || out.error === \"event_archive_disabled\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"archive_file_too_large\") {\n sendJson(res, 413, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplay) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await resolveGenesisReplayRouterV1({\n from: fromSeq,\n to: toSeq,\n type,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayDiff) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const out = await computeGenesisReplayTemporalDiffV1({ from: fromSeq, to: toSeq, type });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEquivalence) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from1 = parseInt(String(u.searchParams.get(\"from1\") || \"\").trim(), 10);\n const to1 = parseInt(String(u.searchParams.get(\"to1\") || \"\").trim(), 10);\n const from2 = parseInt(String(u.searchParams.get(\"from2\") || \"\").trim(), 10);\n const to2 = parseInt(String(u.searchParams.get(\"to2\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await compareGenesisReplayEquivalenceV1({\n from1,\n to1,\n from2,\n to2,\n type,\n includeCheckpoints\n });\n if (\n !out.ok &&\n (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")\n ) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayAnalytics) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayAnalyticsV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEvolution) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const collapseWindows = parseInt(String(u.searchParams.get(\"collapseWindows\") || \"0\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayEvolutionV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n collapseWindows,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/metrics\") {\n res.writeHead(200, { \"Content-Type\": \"text/plain; version=0.0.4; charset=utf-8\" });\n res.end(renderInfraMetricsProm());\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.infraPrometheusMetrics) {\n if (process.env.CASTLE_PROMETHEUS_METRICS !== \"1\") {\n sendJson(res, 404, { ok: false, error: \"prometheus_metrics_disabled\" });\n return;\n }\n res.writeHead(200, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(renderRhizohPrometheusMetrics());\n return;\n }\n if (req.method === \"GET\" && pathname.startsWith(\"/infra/traces/\")) {\n const traceId = decodeURIComponent(pathname.slice(\"/infra/traces/\".length));\n const item = await getTraceById(traceId);\n if (!item) return sendJson(res, 404, { ok: false, error: \"trace_not_found\" });\n return sendJson(res, 200, { ok: true, trace: item });\n }\n if (req.method === \"GET\" && pathname === \"/infra/traces\") {\n const u = new URL(String(req.url || \"/infra/traces\"), `http://${req.headers.host || \"localhost\"}`);\n const sessionId = String(u.searchParams.get(\"sessionId\") || \"\").trim();\n const items = sessionId ? await listTracesBySession(sessionId) : [];\n return sendJson(res, 200, { ok: true, sessionId, traces: items });\n }\n\n /** Presence room mesh (C): JOIN / LEAVE / SNAPSHOT / DELTA / REPLAY (HTTP); SUBSCRIBE = SSE stream. */\n if (req.method === \"POST\" && pathname === \"/presence/mesh/join\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid, clientUid: actor.clientUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/leave\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshLeave(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/snapshot\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n sendJson(res, 200, meshSnapshot(roomUid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/delta\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, MAX_MESSAGE_BYTES);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const seq = meshAppendDelta(roomUid, actor.clientUid, {\n node: body?.node,\n projectionPatch: body?.projectionPatch,\n writerSubject: body?.writerSubject\n });\n sendJson(res, 200, { ok: true, roomUid, seq });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/replay\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const fromNodeId = body?.fromNodeId != null ? String(body.fromNodeId) : \"\";\n const fromSeq = body?.fromSeq;\n if (fromNodeId) {\n sendJson(res, 200, meshReplayFromNodeId(roomUid, fromNodeId));\n return;\n }\n sendJson(res, 200, meshReplayFromSeq(roomUid, fromSeq));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/presence/mesh/subscribe\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const roomUid = String(q.searchParams.get(\"roomUid\") || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n const write = (chunk) => {\n try {\n res.write(chunk);\n } catch {\n /* closed */\n }\n };\n const unsub = meshSubscribeSse(roomUid, write);\n const onClose = () => {\n unsub();\n meshLeave(roomUid, actor.clientUid);\n try {\n res.end();\n } catch {\n /* ignore */\n }\n };\n req.on(\"close\", onClose);\n req.on(\"aborted\", onClose);\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, uid: auth.uid, mode: auth.mode, items: await listConnections(auth.uid) });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const id = await createConnection(auth.uid, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"create_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n await updateConnection(auth.uid, id, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"update_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"DELETE\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n await deleteConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"delete_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url?.startsWith(\"/rhizoh/network/presence\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const u = new URL(req.url, \"http://localhost\");\n const roomKey = String(u.searchParams.get(\"room\") || \"world_space_c2c_v0\").slice(0, 64);\n sendJson(res, 200, {\n ok: true,\n roomKey,\n presence: listCastleNetworkPresenceV0(roomKey)\n });\n } catch (error) {\n sendJson(res, 500, { ok: false, error: error?.message || \"presence_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/rhizoh/sync/vault\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const snapshot = await getCloudSyncSnapshotV0(auth.uid);\n sendJson(res, 200, { ok: true, uid: auth.uid, snapshot });\n } catch (error) {\n sendJson(res, 500, { ok: false, error: error?.message || \"cloud_sync_pull_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/rhizoh/sync/vault\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const snapshot = await mergeCloudSyncSnapshotV0(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, uid: auth.uid, snapshot });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"cloud_sync_push_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections/test\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n if (!checkHttpRateLimit(`llm_conn_test:${auth.uid}`, RL_LLM_CONN_TEST_PER_MIN, 60_000)) {\n return sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n }\n const hasBodyKey = !!String(payload?.apiKey || \"\").trim();\n const result = await queryRhizohLlm(\n {\n message: String(payload?.message || \"Ping from Castle Gateway\"),\n context: payload?.context || { source: \"connection_test\" },\n provider: payload?.provider,\n model: payload?.model,\n apiKey: payload?.apiKey || \"\"\n },\n { allowExternalApiKey: true, llmKeySource: hasBodyKey ? \"auto\" : \"env\" }\n );\n logLlmAccess({\n route: \"/llm/connections/test\",\n uid: auth.uid,\n llmKeyBillingOwner: result.llmKeyBillingOwner,\n llmKeyOrigin: result.llmKeyOrigin,\n provider: result.provider,\n model: result.model,\n connectionId: null\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"connection_test_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAgentIdentities(auth.uid);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerAgentIdentity({\n ownerUid: auth.uid,\n agentId: payload?.agentId,\n role: payload?.role,\n personaSeed: payload?.personaSeed || {},\n capabilityLevel: payload?.capabilityLevel\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/agents/identities/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const owns = await getAgentIdentity(id);\n if (!owns || owns.ownerUid !== auth.uid) return sendJson(res, 403, { ok: false, error: \"forbidden\" });\n const payload = await readHttpJson(req);\n const row = await updateAgentIdentity(id, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/academy/events\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/academy/events\"}`);\n const status = String(url.searchParams.get(\"status\") || \"\");\n const items = await listAcademyEvents({ ownerUid: auth.uid, status, limit: 120 });\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/academy/events\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const ev = await queueAcademyEvent({\n type: payload?.type,\n roomId: payload?.roomId,\n ownerUid: auth.uid,\n participants: payload?.participants || [],\n topic: payload?.topic || \"academy\",\n payload: payload?.payload || {}\n });\n sendJson(res, 200, { ok: true, event: ev });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && String(req.url || \"\").startsWith(\"/academy/events/\") && String(req.url || \"\").endsWith(\"/resolve\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const parts = String(req.url || \"\").split(\"/\").filter(Boolean);\n const eventId = parts[2] || \"\";\n try {\n const payload = await readHttpJson(req);\n const out = await resolveAcademyEvent(eventId, payload || {});\n sendJson(res, 200, { ok: true, event: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_resolve_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/memory\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/memory\"}`);\n const scope = url.searchParams.get(\"scope\") === \"agents\" ? \"agents\" : \"users\";\n const id = String(url.searchParams.get(\"id\") || (scope === \"agents\" ? \"\" : auth.uid));\n const kind = String(url.searchParams.get(\"kind\") || \"\");\n const limit = Number(url.searchParams.get(\"limit\") || 40);\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n sendJson(res, 200, { ok: true, scope, id, kind, items: await listMemories({ scope, id, kind, limit }) });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const profile = await getPersonaGoalMemory(auth.uid);\n sendJson(res, 200, { ok: true, profile });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const profile = await setPersonaGoalMemory({\n uid: auth.uid,\n personaPatch: payload?.personaPatch || {},\n goals: Array.isArray(payload?.goals) ? payload.goals : null,\n preferencesPatch: payload?.preferencesPatch || {}\n });\n sendJson(res, 200, { ok: true, profile });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"profile_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/compact\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const out = await autoCompactMemories({ scope, id });\n sendJson(res, 200, { ok: true, scope, id, ...out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_compact_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/context\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const context = await getMemoryContext({\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n query: String(payload?.query || \"\"),\n limit: Number(payload?.limit || 80)\n });\n sendJson(res, 200, { ok: true, context });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_context_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/place-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const placeName = String(payload?.placeName || \"\").trim();\n if (!placeName) return sendJson(res, 400, { ok: false, error: \"placeName_required\" });\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const persona = String(payload?.waypointPersona || payload?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(placeName)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Place: ${placeName}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Give a concise conversational briefing about this place for a walking-route companion.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-place-brief\", placeName, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `PLACE_BRIEF:${placeName} => ${String(summary.reply || \"\").slice(0, 900)}`,\n tags: [\"place\", \"brief\", \"event-layer\"],\n importance: 0.62,\n meta: { placeName }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"place-brief\",\n text: `${placeName} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: {\n placeName,\n directive: summary.directive,\n lat: hit?.lat ? Number(hit.lat) : null,\n lon: hit?.lon ? Number(hit.lon) : null\n }\n });\n sendJson(res, 200, { ok: true, place: hit || null, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"place_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/route-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const waypoints = Array.isArray(payload?.waypoints) ? payload.waypoints.slice(0, 24) : [];\n const out = [];\n for (const wp of waypoints) {\n const name = String(wp?.name || \"\").trim();\n if (!name) continue;\n const persona = String(wp?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(name)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Waypoint: ${name}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Generate a concise spoken briefing for route waypoint.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-route-brief\", waypoint: name, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n out.push({\n name,\n persona,\n place: hit || null,\n reply: summary.reply,\n directive: summary.directive\n });\n }\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `ROUTE_BRIEF: waypoints=${out.length} ${out.map((x) => `${x.name}:${x.persona}`).join(\" | \")}`.slice(0, 1300),\n tags: [\"route\", \"waypoint\", \"brief\", \"event-layer\"],\n importance: 0.74,\n meta: { count: out.length }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"route-brief\",\n text: out.map((x) => `${x.name}:${String(x.reply || \"\").slice(0, 140)}`).join(\" | \").slice(0, 1400),\n roomId: \"studio-main\",\n meta: {\n waypointCount: out.length,\n waypoints: out.map((x) => ({\n name: x.name,\n persona: x.persona,\n lat: x.place?.lat ? Number(x.place.lat) : null,\n lon: x.place?.lon ? Number(x.place.lon) : null\n }))\n }\n });\n sendJson(res, 200, { ok: true, items: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"route_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/pdf-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const title = String(payload?.title || \"document\");\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const providedText = String(payload?.text || \"\");\n const url = String(payload?.url || \"\");\n let content = providedText;\n if (!content && url) content = await fetchTextFromUrl(url);\n if (!content) return sendJson(res, 400, { ok: false, error: \"text_or_supported_url_required\" });\n const summary = await queryRhizohLlm(\n {\n message: `Summarize this document and provide key points + practical actions.\\nTitle:${title}\\nContent:\\n${content.slice(0, 12000)}`,\n context: { source: \"event-layer-pdf-brief\", title },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `DOC_BRIEF:${title} => ${String(summary.reply || \"\").slice(0, 1000)}`,\n tags: [\"document\", \"brief\", \"library\", \"event-layer\"],\n importance: 0.7,\n meta: { title, url: url || null }\n });\n await addTranscript(auth.uid, {\n source: \"library-brief\",\n eventType: \"pdf-brief\",\n text: `${title} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { title }\n });\n sendJson(res, 200, { ok: true, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/pdf/upload\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req, 8 * 1024 * 1024);\n const job = await enqueuePdfJob({\n uid: auth.uid,\n fileName: payload?.fileName || \"document.pdf\",\n bytesBase64: payload?.contentBase64\n });\n sendJson(res, 200, { ok: true, job });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_upload_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/session\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createPublishSession(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session\",\n text: `Publish session armed: ${row.protocol} ${row.target}`.slice(0, 500),\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: row.id, bridge: row.bridge }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/studio/publish/sessions\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listPublishSessions(auth.uid, 80);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/health-check\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const protocol = String(payload?.protocol || \"WHIP\");\n const target = String(payload?.target || \"\");\n const health = await checkPublishTargetHealth(protocol, target);\n if (payload?.sessionId) {\n await updatePublishSession(auth.uid, String(payload.sessionId), {\n status: health.ok ? \"healthy\" : \"degraded\",\n reconnectPolicy: {\n enabled: true,\n backoffSec: [2, 5, 10, 20],\n nextRetryInSec: health.reconnectInSec || 0,\n maxRetries: 8,\n reason: health.reason\n }\n });\n }\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-health\",\n text: `${protocol} ${target} => ${health.ok ? \"healthy\" : \"degraded\"} (${health.reason})`,\n roomId: \"studio-main\",\n meta: { protocol, target, reconnect: !!health.shouldReconnect }\n });\n sendJson(res, 200, { ok: true, health });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_health_check_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/studio/publish/sessions/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updatePublishSession(auth.uid, id, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session-update\",\n text: `Session ${id} -> ${row.status || \"updated\"}`,\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: id, status: row.status, ingestUrl: row.ingestUrl || \"\" }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/transcripts\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/transcripts\"}`);\n const limit = Math.max(1, Math.min(200, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await listTranscripts(auth.uid, limit);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/transcripts\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await addTranscript(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"transcript_write_failed\" });\n }\n return;\n }\n\n /** E1: broadcast intent → GreenRoom capability → transcript + memory artifact */\n if (req.method === \"POST\" && req.url === \"/studio/capabilities/greenroom\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const traceId = String(payload?.traceId || `TRC-${Date.now().toString(36).toUpperCase()}`);\n const title = String(payload?.title || \"Castle Live Broadcast\").slice(0, 200);\n const intentRaw = String(payload?.intentRaw || \"\").slice(0, 800);\n const audienceEstimate = Math.max(0, Math.min(50_000_000, Number(payload?.audienceEstimate) || 1200));\n const roomId = String(payload?.roomId || \"greenroom-main\");\n const transcriptRow = await addTranscript(auth.uid, {\n source: \"rhizoh\",\n eventType: \"BROADCAST_ROUTED\",\n text: `GreenRoom · ${title}${intentRaw ? `\\nIntent: ${intentRaw}` : \"\"}`,\n roomId,\n meta: {\n ack: \"BROADCAST_ROUTED\",\n capability: \"greenroom\",\n intentType: \"broadcast\",\n traceId,\n audienceEstimate,\n artifact: {\n kind: \"broadcast_card\",\n title,\n traceId,\n roomId,\n createdAt: Date.now()\n }\n }\n });\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n text: `GREENROOM_BROADCAST:${title} trace=${traceId}`,\n tags: [\"broadcast\", \"greenroom\", \"artifact\", \"castle-library\"],\n importance: 0.72,\n kind: \"episodic\",\n meta: { traceId, capability: \"greenroom\", transcriptId: transcriptRow.id }\n });\n sendJson(res, 200, {\n ok: true,\n ack: \"BROADCAST_ROUTED\",\n traceId,\n transcript: transcriptRow,\n replayPath: `/replay/${encodeURIComponent(traceId)}`,\n sharePath: `/replay/${encodeURIComponent(traceId)}`\n });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"greenroom_capability_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/metadata/search\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/metadata/search\"}`);\n const q = String(url.searchParams.get(\"q\") || \"\");\n const limit = Math.max(1, Math.min(300, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await searchMetadata(auth.uid, q, limit);\n sendJson(res, 200, { ok: true, q, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerDevice(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, device: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"device_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const lat = Number(payload?.lat);\n const lon = Number(payload?.lon);\n const speed = Math.max(0, Number(payload?.speed || 0));\n const cfgCenterLat = Number(process.env.CASTLE_GEOFENCE_CENTER_LAT || 41.0082);\n const cfgCenterLon = Number(process.env.CASTLE_GEOFENCE_CENTER_LON || 28.9784);\n const cfgRadiusM = Math.max(100, Number(process.env.CASTLE_GEOFENCE_RADIUS_M || 55000));\n const cfgSpeedCap = Math.max(1, Number(process.env.CASTLE_SPEED_CAP_MPS || 35));\n const dist = Number.isFinite(lat) && Number.isFinite(lon) ? haversineMeters(cfgCenterLat, cfgCenterLon, lat, lon) : 0;\n const geofenceOk = !Number.isFinite(dist) ? true : dist <= cfgRadiusM;\n const speedOk = speed <= cfgSpeedCap;\n const failsafe = !(geofenceOk && speedOk);\n const safety = {\n geofenceOk,\n speedOk,\n failsafe,\n distanceFromCenterM: Number.isFinite(dist) ? dist : null,\n speedCapMps: cfgSpeedCap\n };\n const row = await appendTelemetry(auth.uid, deviceId, {\n lat,\n lon,\n speed,\n battery: payload?.battery,\n mode: payload?.mode || (failsafe ? \"FAILSAFE_HOLD\" : \"ACTIVE\"),\n raw: payload?.raw || {}\n });\n await updateDevice(auth.uid, deviceId, {\n status: failsafe ? \"failsafe\" : \"active\",\n lastTelemetryAt: row.ts,\n safetyState: safety\n });\n if (failsafe) {\n await addTranscript(auth.uid, {\n source: \"robotics-guardrails\",\n eventType: \"failsafe\",\n text: `Device ${deviceId} failsafe triggered geofenceOk=${geofenceOk} speedOk=${speedOk}`,\n roomId: \"studio-main\",\n meta: { deviceId, safety }\n });\n }\n sendJson(res, 200, { ok: true, telemetry: row, safety, command: failsafe ? \"FAILSAFE_HOLD\" : \"CONTINUE\" });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"telemetry_ingest_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listTelemetry(auth.uid, 150);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createAutomation(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"automation-scheduled\",\n text: `Automation scheduled: ${row.title} at ${new Date(row.scheduleAt).toLocaleString()}`,\n roomId: \"studio-main\",\n meta: { automationId: row.id, action: row.action, deviceId: row.deviceId }\n });\n sendJson(res, 200, { ok: true, automation: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"automation_create_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAutomations(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listDevices(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const devs = await listDevices(auth.uid, 500);\n const d = devs.find((x) => x.id === deviceId);\n if (!d) return sendJson(res, 404, { ok: false, error: \"device_not_found\" });\n const action = String(payload?.action || \"\");\n const caps = Array.isArray(d.capabilityProfile) ? d.capabilityProfile : [];\n if (action && caps.length && !caps.includes(action) && !caps.includes(\"*\")) {\n return sendJson(res, 400, { ok: false, error: \"unsupported_action_for_device\", capabilityProfile: caps });\n }\n const cmd = await enqueueCommand(auth.uid, {\n deviceId,\n action: action || \"NOOP\",\n params: payload?.params || {},\n adapter: payload?.adapter || d.adapter || \"websocket\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-command-queue\",\n eventType: \"command-queued\",\n text: `${cmd.deviceId} -> ${cmd.action}`,\n roomId: \"studio-main\",\n meta: { commandId: cmd.id, adapter: cmd.adapter }\n });\n sendJson(res, 200, { ok: true, command: cmd });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listCommands(auth.uid, 200);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/robotics/commands/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updateCommandStatus(auth.uid, id, payload?.status || \"sent\");\n sendJson(res, 200, { ok: true, command: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/rhizoh/bridge\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const result = await runRhizohBrainV2({\n message: String(payload?.message || \"robotics bridge command\"),\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n worldState: payload?.worldState || {},\n deviceState: payload?.deviceState || {},\n intent: \"robotics-mechanics\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-bridge\",\n eventType: \"robotics-command\",\n text: `Bridge -> ${String(result.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { directive: result.directive, agentId: payload?.agentId || \"\" }\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"robotics_bridge_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/planner/graph\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const graph = buildTaskActionGraph(String(payload?.task || \"\"), payload?.context || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"planner-graph\",\n text: `Planner graph built: ${graph.taskTitle} nodes=${graph.nodes.length}`,\n roomId: \"studio-main\",\n meta: { taskTitle: graph.taskTitle, nodeCount: graph.nodes.length }\n });\n sendJson(res, 200, { ok: true, graph });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"planner_graph_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerSocialChannel(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, channel: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_channel_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listSocialChannels(auth.uid, 100);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/broadcast\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const text = String(payload?.text || \"\").slice(0, 1200);\n const channels = await listSocialChannels(auth.uid, 100);\n const enabled = channels.filter((c) => c.status === \"enabled\");\n const deliveries = [];\n for (const c of enabled) {\n const out = await deliverSocialWithRetry(c, text);\n deliveries.push({ id: c.id, platform: c.platform, ...out });\n }\n const okCount = deliveries.filter((d) => d.ok).length;\n await addTranscript(auth.uid, {\n source: \"social-layer\",\n eventType: \"social-broadcast\",\n text: `Broadcast delivered ${okCount}/${enabled.length} channel(s): ${text.slice(0, 240)}`,\n roomId: \"studio-main\",\n meta: { deliveries }\n });\n sendJson(res, 200, { ok: true, sentChannels: okCount, totalChannels: enabled.length, deliveries });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_broadcast_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/ethics/programs\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, ...buildEthicsPrograms() });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands/dispatch\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const all = await listCommands(auth.uid, 200);\n const queued = all.filter((c) => String(c.status || \"queued\") === \"queued\").slice(0, 50);\n const devices = await listDevices(auth.uid, 500);\n const byId = new Map(devices.map((d) => [d.id, d]));\n const results = [];\n for (const cmd of queued) {\n const dev = byId.get(cmd.deviceId);\n if (!dev) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: \"device_not_found\" });\n continue;\n }\n try {\n const dispatched = await dispatchCommandToAdapter(dev, cmd);\n await updateCommandStatus(auth.uid, cmd.id, \"sent\");\n results.push({ id: cmd.id, ok: true, adapter: dispatched.adapter, via: dispatched.via });\n } catch (error) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: String(error?.message || \"dispatch_failed\") });\n }\n }\n await addTranscript(auth.uid, {\n source: \"robotics-command-dispatch\",\n eventType: \"command-dispatch-batch\",\n text: `Dispatch batch size=${results.length} ok=${results.filter((x) => x.ok).length}`,\n roomId: \"studio-main\",\n meta: { results: results.slice(0, 30) }\n });\n sendJson(res, 200, { ok: true, processed: results.length, results });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"dispatch_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/pdf/jobs/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n const job = await getPdfJob(auth.uid, id);\n if (!job) return sendJson(res, 404, { ok: false, error: \"pdf_job_not_found\" });\n sendJson(res, 200, { ok: true, job });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const row = await appendMemory({\n scope,\n id,\n text: payload?.text,\n tags: payload?.tags || [],\n importance: payload?.importance,\n kind: payload?.kind || \"episodic\",\n meta: { uid: auth.uid, ...(payload?.meta || {}) }\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.externalTruth) {\n try {\n if (process.env.RHIZOH_EXTERNAL_TRUTH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_truth:${ip}`, RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n sendJson(res, 200, buildRhizohExternalGroundTruthPayload(req));\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.productOutcome) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const rawBody = await readHttpBodyText(req, 64 * 1024);\n const body = rawBody.trim() ? safeJsonParse(rawBody) : null;\n if (!body || typeof body !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"invalid_json_payload\" });\n return;\n }\n const sigHdr = String(req.headers?.[\"x-rhizoh-outcome-signature\"] || \"\").trim();\n const hmacV = verifyRhizohOutcomeHmac(rawBody, sigHdr);\n if (!hmacV.ok) {\n sendJson(res, 401, { ok: false, error: hmacV.error || \"outcome_hmac_invalid\" });\n return;\n }\n const srcTokHdr = String(req.headers?.[\"x-rhizoh-outcome-source-token\"] || \"\").trim();\n const srcV = verifyRhizohOutcomeSourceToken(body.source, srcTokHdr);\n if (!srcV.ok) {\n sendJson(res, 401, { ok: false, error: srcV.error || \"outcome_source_token_invalid\" });\n return;\n }\n const subjectKey = String(body.subjectKey || \"\").trim().slice(0, 128);\n if (\n subjectKey &&\n !checkHttpRateLimit(\n `rhizoh_product_outcome_subj:${subjectKey}`,\n RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN,\n 60_000\n )\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_subject\" });\n return;\n }\n const result = ingestRhizohProductOutcomeHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n schemaVersion: \"1.1.0\",\n aggregate: result.aggregate,\n integrity: result.integrity\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"outcome_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.productOutcomeAggregate) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome_agg:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const u = new URL(String(req.url || \"/\"), \"http://localhost\");\n const cohortId = u.searchParams.get(\"cohortId\") || \"\";\n const decisionFingerprint = u.searchParams.get(\"decisionFingerprint\") || \"\";\n const limit = u.searchParams.get(\"limit\") || \"50\";\n const rows = listRhizohProductOutcomeAggregates({\n cohortId: cohortId || undefined,\n decisionFingerprint: decisionFingerprint || undefined,\n limit: Number(limit) || 50\n });\n sendJson(res, 200, { ok: true, schemaVersion: \"1.1.0\", rows });\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.externalLossBatch) {\n try {\n if (process.env.RHIZOH_EXTERNAL_LOSS_BATCH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_loss_batch:${ip}`, RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const result = ingestRhizohExternalLossBatchHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, { ok: true, accepted: result.accepted ?? 0 });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"batch_failed\") });\n }\n return;\n }\n\n function moderationAdminOk(req) {\n const expected = String(process.env.CASTLE_MODERATION_ADMIN_KEY || \"\").trim();\n if (!expected) return false;\n const got = String(req.headers[\"x-castle-moderation-key\"] || \"\").trim();\n return got.length > 0 && got === expected;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.opsAbuseReport) {\n try {\n const body = await readHttpJson(req, 32 * 1024);\n const auth = await resolveHttpUser(req);\n const entry = submitAbuseReportV0({\n reporterUid: auth.ok ? auth.uid : null,\n targetUid: body?.targetUid ?? null,\n category: body?.category,\n detail: body?.detail,\n traceId: body?.traceId ?? null\n });\n sendJson(res, 202, { ok: true, reportId: entry.id, status: entry.status });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsModerationQueue) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n queue: listModerationQueueV0(100),\n softBlocked: listSoftBlockedUsersV0()\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-block\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const out = softBlockUserV0(body?.uid, body?.reason);\n sendJson(res, out.ok ? 200 : 400, out);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-unblock\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n sendJson(res, 200, removeSoftBlockV0(body?.uid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsAgentSnapshots) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n const limit = Number(new URL(req.url || \"\", \"http://local\").searchParams.get(\"limit\") || 50);\n sendJson(res, 200, { ok: true, snapshots: listRecentAgentSnapshotsV0(limit) });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.academicObservatoryExport) {\n const auth = await resolveHttpUser(req);\n const out = handleAcademicObservatoryExportGetV0(req, auth);\n sendJson(res, out.status, out.body);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsHardeningStatus) {\n const auth = await resolveHttpUser(req);\n const principal = auth.ok ? `uid:${auth.uid}` : null;\n const gclSnap = await getGlobalCostLedgerSnapshotV0(principal);\n const economicStrategy = evaluateEconomicStrategyL3V0(gclSnap);\n const hardeningTenantId = resolveHardeningTenantIdV0(auth, req);\n const unifiedState = await buildUnifiedStateNarrativeV0({\n tenantId: hardeningTenantId ?? undefined,\n platformScope: !hardeningTenantId,\n principal\n });\n sendJson(res, 200, {\n ok: true,\n schema: \"rhizoh.ops.hardening_status.v0\",\n containment: readAgentContainmentConfigV0(),\n cost: readCostContainmentConfigV0(),\n gcl: {\n backend: await getGclBackendStatusV0(),\n snapshot: gclSnap\n },\n economicStrategyL3: economicStrategy,\n phasedRollout: await getPhasedRolloutStatsV0(),\n tier: readPhasedRolloutTierV0(),\n principalCost: auth.ok ? await getCostContainmentStatsV0(principal) : null,\n stressTaxonomy: verifyStressTaxonomyCoverageV0(),\n stressConflictResolution: verifyStressConflictResolutionV0(),\n resolutionStabilityEnvelope: verifyResolutionStabilityEnvelopeV0(),\n unifiedState: {\n tenantScope: {\n tenantId: unifiedState.tenantScope?.tenantId,\n scope: unifiedState.tenantScope?.scope,\n noGlobalDerivedState: unifiedState.tenantScope?.noGlobalDerivedState\n },\n narrativeFingerprint: unifiedState.narrativeFingerprint,\n screenshotScopeWatermark: unifiedState.screenshotScopeWatermark,\n interpretationSafetyContract: unifiedState.interpretationSafetyContract,\n interpretationUxContract: {\n schema: unifiedState.interpretationUxContract?.schema,\n layerOrder: unifiedState.interpretationUxContract?.layerOrder,\n narrativeNeverVisuallyDominant: unifiedState.interpretationUxContract?.narrativeNeverVisuallyDominant\n },\n governance: {\n narrativeIsNotDecisionLayer: unifiedState.governance.narrativeIsNotDecisionLayer,\n displayOrder: unifiedState.governance.displayOrder,\n trustPosture: unifiedState.governance.trustPosture,\n interpretationUxContract: unifiedState.governance.interpretationUxContract?.schema\n },\n humanOps: {\n decisionTier: unifiedState.humanOps?.humanDecisionScaling?.tier,\n decisionOwner: unifiedState.humanOps?.humanDecisionScaling?.decisionOwner,\n opsRouteId: unifiedState.humanOps?.humanDecisionOpsRunbook?.routing?.routeId,\n slaAckMinutes: unifiedState.humanOps?.humanDecisionOpsRunbook?.sla?.ackMinutes,\n uiDisclaimerRequired: unifiedState.humanOps?.narrativeUiSafety?.displayRules?.requireDisclaimerBanner,\n misreadHighResidual: unifiedState.humanOps?.misreadSimulation?.highResidualCount,\n propagationHighResidual: unifiedState.humanOps?.socialPropagationSimulation?.highResidualCount,\n worstWatermarkSurvivability:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.worstWatermarkSurvivability,\n dominantDistortionSource:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.dominantDistortionSource\n },\n culturalRisk: {\n perceptualRiskClass: unifiedState.culturalRisk?.perceptualRiskClass,\n trustFork: unifiedState.culturalRisk?.trustDynamics?.fork,\n trustDecayScore: unifiedState.culturalRisk?.trustDynamics?.scores?.trustDecay,\n mythologyScore: unifiedState.culturalRisk?.trustDynamics?.scores?.mythology,\n confidenceDetachmentExposure:\n unifiedState.culturalRisk?.trustDynamics?.axes?.confidenceDetachmentExposure\n },\n appliedSystemsLayer: {\n schema: unifiedState.appliedSystemsLayer?.schema,\n researchOutputType: unifiedState.appliedSystemsLayer?.researchGovernance?.outputType,\n roboticsBlockActuation: unifiedState.appliedSystemsLayer?.roboticsGrounding?.blockActuation,\n spiralRumorCount:\n unifiedState.appliedSystemsLayer?.spiralMMOGameKernel?.gameLayer?.rumors?.length\n },\n actionSemanticGovernance: {\n schema: unifiedState.actionSemanticGovernance?.schema,\n highRiskActionCount: unifiedState.actionSemanticGovernance?.highRiskActionCount,\n loopClosed: unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.loopClosed,\n authorityCollapseMitigated:\n unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.risks?.authorityCollapse\n ?.mitigated\n },\n actionContextResolution: {\n schema: unifiedState.actionContextResolution?.schema,\n contextFingerprint: unifiedState.actionContextResolution?.contextFingerprint?.fingerprintHash,\n domainCollision: unifiedState.actionContextResolution?.domainCollision?.detected,\n deployAgentSpiralBlocked:\n unifiedState.actionContextResolution?.actionOverloadingRisk?.deployAgentExample\n ?.spiral_mmo === \"blocked\",\n roboticsActuationEligible:\n unifiedState.actionContextResolution?.roboticsActuationCorrectness?.actuationEligible\n },\n epistemicCoherence: {\n schema: unifiedState.epistemicCoherence?.schema,\n verdict: unifiedState.epistemicCoherence?.systemCoherence?.verdict,\n coherenceScore: unifiedState.epistemicCoherence?.systemCoherence?.score,\n contradictionCount: unifiedState.epistemicCoherence?.crossLayerContradictions?.count,\n operatorDecision: unifiedState.epistemicCoherence?.uxCompression?.operatorDecision,\n headlineTr: unifiedState.epistemicCoherence?.uxCompression?.headline?.tr,\n falseSafetyWarning: Boolean(\n unifiedState.epistemicCoherence?.uxCompression?.falseSafetyWarning\n )\n },\n coherenceAuthorityBoundary: {\n schema: unifiedState.coherenceAuthorityBoundary?.schema,\n bindingValid: unifiedState.coherenceAuthorityBoundary?.eclAuthorityBinding?.valid,\n canExecute: unifiedState.coherenceAuthorityBoundary?.contract?.can_execute,\n mandatoryBannerTr:\n unifiedState.coherenceAuthorityBoundary?.disclaimers?.mandatoryBanner?.tr\n },\n decisionLatencyGovernance: {\n schema: unifiedState.decisionLatencyGovernance?.schema,\n latencyTier: unifiedState.decisionLatencyGovernance?.latencyTier,\n inflationScore: unifiedState.decisionLatencyGovernance?.latencyInflation?.score,\n fastPathEligible: unifiedState.decisionLatencyGovernance?.fastPath?.eligible,\n primaryActionId: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.primaryActionId,\n ackSlaMinutes: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.ackSlaMinutes,\n certaintyCap: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.certaintyCap,\n uncertaintyItems:\n unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.uncertaintyEnvelope?.itemCount\n },\n decisionPacketUncertaintyBoundary: {\n schema: unifiedState.decisionPacketUncertaintyBoundary?.schema,\n invariantValid: unifiedState.decisionPacketUncertaintyBoundary?.invariantCheck?.valid,\n centralInvariant: unifiedState.decisionPacketUncertaintyBoundary?.centralInvariant\n },\n epistemicDecisionPacing: {\n schema: unifiedState.epistemicDecisionPacing?.schema,\n queueSaturationTier:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation?.tier,\n operatorProcessingLatency:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation\n ?.operatorProcessingLatency,\n postureWindowCount:\n unifiedState.epistemicDecisionPacing?.temporaryStaticPostureWindows\n ?.temporaryStaticPostureWindows?.windowCount,\n executionBoundariesValid:\n unifiedState.epistemicDecisionPacing?.executionBoundaries?.valid,\n grantsExecutionApproval: unifiedState.epistemicDecisionPacing?.grantsExecutionApproval\n },\n epistemicTemporalCoherence: {\n schema: unifiedState.epistemicTemporalCoherence?.schema,\n allInvariantsValid: unifiedState.epistemicTemporalCoherence?.allInvariantsValid,\n temporalAlignmentValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.temporalAlignment?.valid,\n crossWindowGuardValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard?.valid,\n delayedTruthValid: unifiedState.epistemicTemporalCoherence?.invariants?.delayedTruthRule?.valid,\n reconciliationRequired:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard\n ?.reconciliation?.required\n },\n realityDriftObserver: {\n schema: unifiedState.realityDriftObserver?.schema,\n driftActive: unifiedState.realityDriftObserver?.driftObserverSummary?.active,\n misapprehensionShapeCount:\n unifiedState.realityDriftObserver?.misapprehensionShapeCatalog?.shapeCount,\n criticalShapeCount:\n unifiedState.realityDriftObserver?.driftObserverSummary?.criticalShapeCount,\n roboticsMismatchActive:\n unifiedState.realityDriftObserver?.roboticsFeedbackMismatch?.active\n },\n driftCausality: {\n schema: unifiedState.driftCausality?.schema,\n shapesWithCausalExplanation: unifiedState.driftCausality?.rdolLinkage?.shapesWithCausalExplanation,\n activeDomainCount: unifiedState.driftCausality?.causalitySummary?.activeDomainCount,\n propagationCausalityActive: unifiedState.driftCausality?.causalDomains?.propagation?.active\n },\n productReadiness: unifiedState.productReadiness,\n stateLayers: {\n raw: unifiedState.stateLayers.raw,\n derived: {\n systemState: unifiedState.stateLayers.derived.systemState,\n narrative: unifiedState.stateLayers.derived.narrative,\n validation: unifiedState.stateLayers.derived.validation\n },\n policy: {\n binding: unifiedState.stateLayers.policy.binding,\n decisionOwner: unifiedState.stateLayers.policy.decisionOwner,\n prohibitedActions: unifiedState.stateLayers.policy.prohibitedActions\n }\n },\n systemState: unifiedState.systemState,\n interpretation: unifiedState.interpretation,\n validation: {\n trustPosture: unifiedState.validation.trustPosture,\n adjustedConfidence: unifiedState.validation.adjustedConfidence,\n agreement: unifiedState.validation.confidenceDecomposition.agreement,\n divergenceScore: unifiedState.validation.divergence.divergenceScore,\n validated: unifiedState.validation.divergence.validated,\n uncertaintyTags: unifiedState.validation.uncertainty.tags.map((t) => t.id),\n flags: unifiedState.validation.divergence.flags.map((f) => f.id)\n },\n compression: unifiedState.compression\n }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 6 * 1024 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (\n !checkHttpRateLimit(`rhizoh_voice_transcribe:${rlKey}`, RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN, 60_000)\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const voiceEnv = rhizohVoiceTranscribeEnvV3();\n if (!voiceEnv.any) {\n sendJson(res, 503, {\n ok: false,\n error: \"voice_asr_not_configured\",\n env: voiceEnv\n });\n return;\n }\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n const result = await runRhizohVoiceTranscribeV3(\n applyLanguagePropagationToVoiceBodyV1(payload, langProp)\n );\n sendJson(res, result.ok ? 200 : 422, {\n ...result,\n traceId: String(payload?.traceId || \"\"),\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n atMs: Date.now()\n });\n } catch (error) {\n sendJson(res, 500, {\n ok: false,\n error: \"voice_transcribe_failed\",\n detail: String(error?.message || error),\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(\"/rhizoh/llm/task/\")) {\n try {\n const rawTaskId = decodeURIComponent(pathname.slice(\"/rhizoh/llm/task/\".length).split(\"/\")[0] || \"\");\n const taskId = String(rawTaskId || \"\").toLowerCase() === \"last\" ? getLatestLlmWorkerTaskIdV0() : rawTaskId;\n const snap = taskId ? getLlmWorkerTaskSnapshotV0(taskId) : null;\n if (!snap) {\n sendJson(res, 404, {\n ok: false,\n error: \"llm_task_not_found\",\n taskId: rawTaskId,\n status: \"missing\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n if (snap.status === \"processing\") {\n sendJson(res, 202, {\n ok: true,\n taskId: snap.taskId,\n status: \"processing\",\n createdAt: snap.createdAt,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n // Terminal states (completed/failed) always HTTP 200 — poll must not look like server fault (500).\n sendJson(res, 200, {\n ...(snap.body || { ok: false, error: \"llm_task_empty\", taskId: snap.taskId }),\n taskId: snap.taskId,\n status: snap.status === \"completed\" ? \"completed\" : snap.status,\n terminal: true,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n } catch (error) {\n console.error(\"[llm-task-poll] handler failed:\", error);\n sendJson(res, 200, {\n ok: false,\n error: \"llm_task_poll_handler_failed\",\n detail: String(error?.message || error),\n status: \"failed\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === rhizohRuntime.routes.rhizohLlm) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 128 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n logRhizohHealth(\"gateway_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: String(payload?.traceId || \"\"),\n auth: auth.ok ? \"ok\" : \"anon\",\n ip\n });\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (!checkHttpRateLimit(`rhizoh_llm:${rlKey}`, RL_RHIZOH_LLM_PER_MIN, 60_000)) {\n const rlTaxonomy = classifyStressResponseV0({ code: \"rate_limit_exceeded\" });\n return sendJson(res, 429, {\n ok: false,\n error: \"rate_limit_exceeded\",\n reply: rlTaxonomy.userMessageTr || \"İstek sınırı aşıldı. Kısa süre sonra tekrar deneyin.\",\n stressClass: rlTaxonomy.stressClass,\n responseAction: rlTaxonomy.responseAction,\n stressMatrix: rlTaxonomy.matrix,\n stressInterpretable: rlTaxonomy.interpretable,\n stressConfidence: rlTaxonomy.stressConfidence,\n actionConfidence: rlTaxonomy.actionConfidence,\n actionSoftened: rlTaxonomy.actionSoftened,\n responseActionStrict: rlTaxonomy.responseActionStrict,\n actionInterpretable: rlTaxonomy.actionInterpretable,\n conflictResolution: rlTaxonomy.conflictResolution,\n directive: \"NONE\"\n });\n }\n\n const requireExplicit = process.env.CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE === \"1\";\n const keyMode = normalizeClientLlmKeySource(payload?.llmKeySource ?? payload?.keySource);\n if (keyMode === null) {\n return sendJson(res, 400, {\n ok: false,\n error: \"invalid_llm_key_source\",\n reply: \"llmKeySource geçersiz. Kullanın: env | user_connection | auto\",\n directive: \"NONE\"\n });\n }\n if (requireExplicit && keyMode === \"auto\") {\n return sendJson(res, 400, {\n ok: false,\n error: \"llm_key_source_required\",\n reply: \"Sunucu CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE=1: istekte llmKeySource (env veya user_connection) zorunlu.\",\n directive: \"NONE\"\n });\n }\n\n const conn = auth.ok ? await resolveConnection(auth.uid, String(payload?.connectionId || \"\")) : null;\n\n if (keyMode === \"user_connection\") {\n if (!auth.ok) {\n return sendJson(res, 401, {\n ok: false,\n error: \"llm_auth_required_for_user_connection\",\n reply: \"Kayıtlı LLM bağlantısı için giriş gerekli.\",\n directive: \"NONE\"\n });\n }\n if (!conn?.apiKey) {\n return sendJson(res, 400, {\n ok: false,\n error: \"user_llm_connection_required\",\n reply: \"Geçerli bir LLM bağlantısı yok; Studio’da bağlantı ekleyin veya llmKeySource=env kullanın.\",\n directive: \"NONE\"\n });\n }\n }\n\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n let safePayload = applyLanguagePropagationToLlmPayloadV1({ ...(payload || {}) }, langProp);\n const trimOut = trimRhizohLlmGatewayPayloadV1(safePayload);\n if (trimOut.meta.trimmed) {\n logRhizohHealth(\"llm_payload_trimmed\", {\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n bytesBefore: trimOut.meta.bytesBefore,\n bytesAfter: trimOut.meta.bytesAfter\n });\n }\n safePayload = trimOut.payload;\n delete safePayload.apiKey;\n delete safePayload.llmKeySource;\n delete safePayload.keySource;\n\n const llmMessage = String(\n safePayload?.message || safePayload?.text || payload?.message || payload?.text || \"\"\n ).trim();\n if (!llmMessage) {\n return sendJson(res, 400, {\n ok: false,\n error: \"message_required\",\n reply: \"Mesaj metni gerekli (message alanı boş).\",\n directive: \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n if (!safePayload.message) {\n safePayload = { ...safePayload, message: llmMessage };\n }\n\n let resolvedProvider = payload?.provider;\n let resolvedModel = payload?.model;\n let connApiKey = \"\";\n\n ({\n resolvedProvider,\n resolvedModel,\n connApiKey\n } = resolveServerLlmProviderV0(keyMode, conn, payload));\n\n const turnInput = {\n safePayload,\n auth,\n keyMode,\n conn,\n resolvedProvider,\n resolvedModel,\n connApiKey\n };\n const turnMeta = {\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n langEcho: buildRhizohLanguagePropagationEchoV1(langProp)\n };\n\n if (\n isLlmWorkerEnabledV0() &&\n isLlmWorkerAsyncEnabledV0() &&\n String(req.headers[\"x-rhizoh-llm-sync\"] || \"\").trim() !== \"1\"\n ) {\n const taskId = enqueueLlmWorkerTaskAsyncV0(turnInput, turnMeta);\n logRhizohHealth(\"llm_worker_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n taskId,\n traceId: String(payload?.traceId || taskId),\n keyMode,\n async: true\n });\n sendJson(res, 202, {\n ok: true,\n taskId,\n status: \"processing\",\n traceId: String(payload?.traceId || taskId),\n pollPath: `/rhizoh/llm/task/${taskId}`,\n gatewayPid: process.pid,\n ...turnMeta.langEcho\n });\n return;\n }\n\n const { result, traceId, turnLatencyMs, spinePhases, sampledTrace } = isLlmWorkerEnabledV0()\n ? await runLlmWorkerTaskV0(turnInput, turnMeta)\n : await rhizohGatewayTurn(turnInput);\n logRhizohHealth(\"llm_response\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId,\n turnLatencyMs: Number(turnLatencyMs || 0),\n keyMode\n });\n sendJson(res, 200, {\n ...result,\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n traceId,\n turnLatencyMs,\n ...(spinePhases ? { spinePhases } : {}),\n ...(sampledTrace !== undefined ? { sampledTrace } : {})\n });\n } catch (error) {\n const msg = String(error?.message || \"\");\n const code = error?.code || \"\";\n let status = 500;\n if (msg === \"payload_too_large\") status = 413;\n else if (msg === \"rate_limit_exceeded\") status = 429;\n else if (\n code === \"cost_hard_limit\" ||\n code === \"phased_rollout_capacity\" ||\n error?.containment === true ||\n String(code).startsWith(\"agent_\")\n ) {\n status = 429;\n } else if (code === \"user_soft_blocked\" || code === \"prompt_abuse_detected\") status = 403;\n else if (\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(code) ||\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(msg) ||\n msg.includes(\"missing_api_key_for_\")\n )\n status = 400;\n let rhizohFailureKind = \"provider_error\";\n if (msg === \"rate_limit_exceeded\" || status === 429) rhizohFailureKind = \"rate_limit\";\n else if (status === 403) rhizohFailureKind = \"policy_block\";\n else if (status === 400 || status === 413) rhizohFailureKind = \"client_config\";\n else if (msg.startsWith(\"provider_http_\")) {\n const st = Number(msg.replace(\"provider_http_\", \"\"));\n if (st === 408 || st === 504) rhizohFailureKind = \"timeout\";\n else rhizohFailureKind = \"provider_error\";\n } else if (/timeout|timed out|ETIMEDOUT/i.test(msg)) rhizohFailureKind = \"timeout\";\n\n const providerHttpMatch = msg.match(/^provider_http_(\\d{3})$/);\n const stressTaxonomy =\n error?.stressClass != null\n ? {\n stressClass: error.stressClass,\n responseAction: error.responseAction,\n stressMatrix: error.stressMatrix,\n stressInterpretable: error.stressInterpretable !== false,\n stressConfidence: error.stressConfidence,\n actionConfidence: error.actionConfidence,\n actionSoftened: error.actionSoftened,\n responseActionStrict: error.responseActionStrict,\n actionInterpretable: error.actionInterpretable,\n conflictResolution: error.conflictResolution,\n stressSecondary: error.stressSecondary,\n responseActions: error.responseActions\n }\n : (() => {\n const t = classifyStressResponseV0({\n code: code || msg,\n rhizohFailureKind,\n providerHttpStatus: providerHttpMatch\n ? Number(providerHttpMatch[1])\n : null\n });\n return {\n stressClass: t.stressClass,\n responseAction: t.responseAction,\n stressMatrix: t.matrix,\n stressInterpretable: t.interpretable,\n stressConfidence: t.stressConfidence,\n actionConfidence: t.actionConfidence,\n actionSoftened: t.actionSoftened,\n responseActionStrict: t.responseActionStrict,\n actionInterpretable: t.actionInterpretable,\n conflictResolution: t.conflictResolution,\n stressSecondary: t.stressSecondary,\n responseActions: t.responseActions\n };\n })();\n sendJson(res, status, {\n ok: false,\n rhizohFailureKind,\n ...(providerHttpMatch ? { providerHttpStatus: Number(providerHttpMatch[1]) } : {}),\n error: code || msg || \"rhizoh_llm_failed\",\n detail: msg || code || undefined,\n reply:\n error?.reply ||\n (status === 500 ? \"Rhizoh bağlantısı geçici olarak kesildi.\" : msg),\n directive: error?.directive || \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n ...stressTaxonomy\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicSeal) {\n try {\n if (!EPISTEMIC_SEAL_SECRET) {\n sendJson(res, 503, { ok: false, error: \"epistemic_seal_disabled\" });\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`epistemic_seal:${ip}`, RL_EPISTEMIC_SEAL_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const truth = body.truth_contract;\n if (!truth || typeof truth !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"truth_contract_required\" });\n return;\n }\n const sealBody = {\n truth_contract: truth,\n runtime_hash: String(body.runtime_hash || \"\"),\n model_route: {\n provider: body.model_route?.provider ?? null,\n model: body.model_route?.model ?? null\n },\n memory_digest: String(body.memory_digest || \"\"),\n world_snapshot_hash: String(body.world_snapshot_hash || \"\"),\n timestamp: Number(body.timestamp) || Date.now()\n };\n const canonical = canonicalEpistemicSealString(sealBody);\n const { hash, signature } = hashAndSignEpistemicSeal(canonical, EPISTEMIC_SEAL_SECRET);\n recordGenesisEpistemicSealIssued(hash);\n const attestation = {\n schema: EPISTEMIC_SEAL_SCHEMA,\n algorithm: \"SHA-256+HMAC-SHA256\",\n keyHint: process.env.CASTLE_EPISTEMIC_SEAL_SECRET ? \"CASTLE_EPISTEMIC_SEAL_SECRET\" : \"CASTLE_GATEWAY_TOKEN\",\n issuedAt: Date.now(),\n gateway: { service: \"castle-gateway\", listenPort: PORT }\n };\n sendJson(res, 200, {\n ok: true,\n hash,\n signature,\n attestation,\n canonicalBytes: Buffer.byteLength(canonical, \"utf8\")\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"seal_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicLogsBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`epistemic_logs:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 120) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const persisted = await persistEpistemicLedgerBatch(auth.uid, entries);\n recordGenesisEpistemicLedgerPersisted(persisted.persisted);\n // Internal cognition layer: forecast stays gateway/ledger-only (no client exposure).\n try {\n await persistEpistemicForecastBatch(auth.uid, persisted.normalized || []);\n } catch {\n /* forecast failures must not break primary ledger ingest */\n }\n sendJson(res, 200, {\n ok: true,\n persisted: persisted.persisted,\n mode: persisted.mode,\n latest: persisted.normalized?.[persisted.normalized.length - 1] || null\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"epistemic_log_ingest_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.authorityLedgerBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`authority_ledger:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 80) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const witnessed = persistAuthorityLedgerWitnessBatchV1(auth.uid, entries, EPISTEMIC_SEAL_SECRET);\n if (witnessed.witnessed > 0) {\n recordGenesisEpistemicLedgerPersisted(witnessed.witnessed);\n const clientSeal = witnessed.lastWitness?.clientSealHash;\n if (clientSeal) recordGenesisEpistemicSealIssued(clientSeal);\n }\n sendJson(res, 200, {\n ok: true,\n witnessed: witnessed.witnessed,\n quarantined: witnessed.quarantined,\n chainHeight: witnessed.chainHeight,\n chainHead: witnessed.chainHead,\n lastWitness: witnessed.lastWitness,\n results: witnessed.results\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"authority_ledger_witness_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.authorityLedgerReplay) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const replayUrl = new URL(String(req.url || \"\"), \"http://localhost\");\n const epochId = String(replayUrl.searchParams.get(\"epochId\") || \"\").trim() || undefined;\n const snapshot = getAuthorityLedgerWitnessSnapshotV1(auth.uid, epochId);\n const replay = replayAuthorityLedgerWitnessChainV1(auth.uid, epochId);\n sendJson(res, 200, {\n ok: true,\n snapshot,\n replay,\n schema: \"castle.rhizoh.authority_ledger_witness.v1.replay_surface\"\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"authority_ledger_replay_failed\") });\n }\n return;\n }\n\n sendJson(res, 404, { ok: false, error: \"not_found\" });\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\nspiralWssForGenesis = wss;\nconst orchestrator = createOrchestrator();\nconst clientStats = new Map();\nlet broadcasterClientId = null;\nconst spiralState = {\n rooms: [{ id: \"room-default\", name: \"Spiral Default Arena\" }],\n activeByClient: new Map(),\n characters: new Map() // id -> {id,name,role,roomId,source}\n};\n\n/** Castle social rooms — keyed by `castleRoomKey` from {@link WS_MESSAGE.CASTLE_SOCIAL_PULSE}. */\nconst castleSocialRoomState = new Map();\nconst CASTLE_SOCIAL_MEMBER_TTL_MS = 90_000;\nconst CASTLE_SOCIAL_MAX_PULSE_BYTES = 8192;\nconst RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES = 5 * 1024 * 1024;\nconst RHIZOH_VOICE_LIVE_MAX_CHUNKS = 96;\n\n/**\n * Voice Engine v3 live media lane.\n * Client streams MediaRecorder chunks over the existing gateway WS; STOP reuses the\n * gateway ASR runner so orchestration remains the single authority downstream.\n */\nconst rhizohVoiceLiveSessions = new WeakMap();\n\nfunction appendRhizohVoiceLiveEventV0(session, type, payloadRefSeed = \"\") {\n if (!session) return null;\n session.eventSeq = (Number(session.eventSeq) || 0) + 1;\n const event = createRhizohImmutableEventV0({\n type,\n sessionId: session.sessionId,\n traceId: session.traceId,\n actorSeed: session.sessionId || session.traceId,\n eventSeq: session.eventSeq,\n payloadRef: createRhizohPayloadRefV0(payloadRefSeed || `${session.sessionId}:${type}:${session.eventSeq}`)\n });\n session.events = [...(session.events || []), event].slice(-64);\n return event;\n}\n\nfunction sendRhizohVoiceLiveEnvelopeV0(socket, type, payload) {\n try {\n if (socket?.readyState === 1) socket.send(JSON.stringify(createEnvelope(type, payload)));\n } catch {\n /* noop */\n }\n}\n\nfunction decodeRhizohVoiceLiveChunkV0(payload = {}) {\n const b64 = String(payload.audioBase64 || \"\").trim();\n if (!b64) return { ok: false, error: \"audio_base64_required\" };\n try {\n const buf = Buffer.from(b64, \"base64\");\n if (!buf.length) return { ok: false, error: \"audio_decode_empty\" };\n return { ok: true, buffer: buf };\n } catch {\n return { ok: false, error: \"audio_base64_invalid\" };\n }\n}\n\nfunction handleRhizohVoiceLiveStartV0(socket, payload = {}) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const sessionId = String(p.sessionId || `live_${Date.now().toString(36)}`).slice(0, 96);\n rhizohVoiceLiveSessions.set(socket, {\n sessionId,\n traceId: String(p.traceId || \"\").slice(0, 128),\n mimeType: String(p.mimeType || \"audio/webm\").slice(0, 80),\n languageCode: String(p.languageCode || \"tr-TR\").slice(0, 32),\n path: String(p.path || \"both\").slice(0, 24),\n chunks: [],\n chunkIndexes: [],\n totalBytes: 0,\n nextChunkIndex: 1,\n eventSeq: 0,\n events: [],\n startedAtMs: Date.now()\n });\n appendRhizohVoiceLiveEventV0(rhizohVoiceLiveSessions.get(socket), \"VOICE_LIVE_SESSION_STARTED\", `${sessionId}:live_session`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_START, {\n ok: true,\n sessionId,\n schema: \"castle.rhizoh.voice_live.gateway_lane.v0\"\n });\n}\n\nfunction handleRhizohVoiceLiveChunkV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const sequence = validateRhizohVoiceLiveChunkPayloadV0(payload, {\n expectedChunkIndex: session.nextChunkIndex\n });\n if (!sequence.ok) {\n rhizohVoiceLiveSessions.delete(socket);\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:chunk_reject`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: sequence.error,\n expectedChunkIndex: sequence.expectedChunkIndex,\n chunkIndex: sequence.chunkIndex\n });\n return;\n }\n const decoded = decodeRhizohVoiceLiveChunkV0(payload);\n if (!decoded.ok) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: decoded.error\n });\n return;\n }\n if (\n session.chunks.length >= RHIZOH_VOICE_LIVE_MAX_CHUNKS ||\n session.totalBytes + decoded.buffer.length > RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES\n ) {\n rhizohVoiceLiveSessions.delete(socket);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_audio_too_large\"\n });\n return;\n }\n session.chunks.push(decoded.buffer);\n session.chunkIndexes.push(sequence.chunkIndex);\n session.totalBytes += decoded.buffer.length;\n session.nextChunkIndex += 1;\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_CHUNK_ACCEPTED\", `${session.sessionId}:chunk:${sequence.chunkIndex}`);\n}\n\nfunction handleRhizohVoiceLiveStopV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n rhizohVoiceLiveSessions.delete(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const lastChunkIndex = Number(payload?.lastChunkIndex);\n if (Number.isInteger(lastChunkIndex) && lastChunkIndex !== session.chunks.length) {\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:stop_mismatch`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_chunk_count_mismatch\",\n expectedChunkCount: lastChunkIndex,\n receivedChunkCount: session.chunks.length\n });\n return;\n }\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_STOP_RECEIVED\", `${session.sessionId}:stop:${session.chunks.length}`);\n const audio = Buffer.concat(session.chunks);\n if (!audio.length) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"audio_empty\"\n });\n return;\n }\n runRhizohVoiceTranscribeV3({\n path: String(payload?.path || session.path || \"both\"),\n audioBase64: audio.toString(\"base64\"),\n mimeType: String(payload?.mimeType || session.mimeType || \"audio/webm\"),\n languageCode: String(payload?.languageCode || session.languageCode || \"tr-TR\"),\n traceId: String(payload?.traceId || session.traceId || \"\"),\n sessionId: session.sessionId,\n transport: \"gateway_ws_live\"\n })\n .then((result) => {\n appendRhizohVoiceLiveEventV0(session, result?.ok ? \"VOICE_TRANSCRIBE_FINAL\" : \"VOICE_TRANSCRIBE_FAILED\", `${session.sessionId}:final`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_FINAL, {\n ...result,\n sessionId: session.sessionId,\n transportPath: \"gateway_ws_live\",\n bytes: audio.length,\n chunkCount: session.chunks.length,\n chunkSequence: Object.freeze({\n ok: true,\n firstChunkIndex: session.chunkIndexes[0] || null,\n lastChunkIndex: session.chunkIndexes[session.chunkIndexes.length - 1] || null,\n receivedChunkCount: session.chunks.length\n }),\n eventTrace: Object.freeze((session.events || []).slice(-16)),\n recordedMs: Math.max(0, Date.now() - session.startedAtMs)\n });\n })\n .catch((e) => {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_transcribe_failed\",\n detail: String(e?.message || e)\n });\n });\n}\n\nfunction removeCastleSocialClientFromAllRooms(clientId) {\n const cid = String(clientId || \"\");\n if (!cid) return;\n for (const room of castleSocialRoomState.values()) {\n room.members.delete(cid);\n }\n}\n\nfunction buildCastleSocialRoomPayload(roomKey) {\n const room = castleSocialRoomState.get(roomKey);\n if (!room) return null;\n const now = Date.now();\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const roster = [...room.members.values()].map((m) => ({\n userId: m.userId,\n lastMs: m.lastMs,\n ...m.sp\n }));\n const peerFeeds =\n room.walPeers && room.walPeers.size\n ? [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }))\n : [];\n return { castleRoomKey: roomKey, seq: room.seq, roster, peerFeeds, ts: now };\n}\n\nconst CASTLE_WAL_PEER_MAX_FEED_BYTES = 48 * 1024;\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {{ ok: boolean, code?: string, reason?: string }} gate\n */\nfunction rejectWalPeerIngressV0(socket, gate) {\n const code = gate.code || \"wal_peer_rejected\";\n recordWalPeerRejectV0(code);\n recordSubstrateOtelEventV0(\"wal_peer_reject\", { code, reason: String(gate.reason || \"\").slice(0, 120) });\n socket.send(\n JSON.stringify(\n createEnvelope(WS_MESSAGE.ERROR, {\n error: gate.reason || \"WAL peer ingress rejected.\",\n code\n })\n )\n );\n}\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {Record<string, unknown> | null | undefined} walPeerFeed\n * @returns {boolean}\n */\nfunction gateWalPeerIngressV0(socket, walPeerFeed) {\n const authGate = assertWalPeerSocketAuthorityV0(socket);\n if (!authGate.ok) {\n rejectWalPeerIngressV0(socket, authGate);\n return false;\n }\n const sigGate = validateWalPeerFeedSignatureV0(\n walPeerFeed && typeof walPeerFeed === \"object\" ? walPeerFeed : {}\n );\n if (!sigGate.ok) {\n rejectWalPeerIngressV0(socket, sigGate);\n return false;\n }\n return true;\n}\n\nfunction handleCastleWalPeerFeed(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_WAL_PEER_MAX_FEED_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_WAL_PEER_FEED too large.\" })));\n return;\n }\n const walPeerFeed = p.walPeerFeed && typeof p.walPeerFeed === \"object\" ? p.walPeerFeed : {};\n if (!gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const castleRoomKey = String(p.castleRoomKey || \"wal:convergence\").slice(0, 64);\n const castleId = String(p.castleId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId,\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: room.members.get(socket.clientId)?.sp ?? {}\n });\n const peerFeeds = [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }));\n const body = {\n castleRoomKey,\n seq: room.seq,\n peerFeeds,\n ts: now\n };\n const env = createEnvelope(WS_MESSAGE.CASTLE_WAL_PEER_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys(), ...room.walPeers.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction handleCastleSocialPulse(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_SOCIAL_MAX_PULSE_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_SOCIAL_PULSE too large.\" })));\n return;\n }\n const castleRoomKey = String(p.castleRoomKey || \"default\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n const sp = p.socialPulse && typeof p.socialPulse === \"object\" ? p.socialPulse : {};\n const walPeerFeed =\n p.walPeerFeed && typeof p.walPeerFeed === \"object\"\n ? p.walPeerFeed\n : sp.walPeerFeed && typeof sp.walPeerFeed === \"object\"\n ? sp.walPeerFeed\n : null;\n if (walPeerFeed && !gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const spNext = walPeerFeed ? { ...sp, walPeerFeed } : { ...sp };\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: spNext\n });\n if (walPeerFeed) {\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId: String(p.castleId || userId).slice(0, 64),\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n }\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const body = buildCastleSocialRoomPayload(castleRoomKey);\n if (!body) return;\n const env = createEnvelope(WS_MESSAGE.CASTLE_SOCIAL_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcast(msgObj) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcastState() {\n broadcast(\n createEnvelope(WS_MESSAGE.BROADCAST_STATE, {\n broadcasterClientId,\n viewers: [...wss.clients].filter((c) => c.readyState === 1).length\n })\n );\n}\n\nfunction currentSpiralStatePayload() {\n return {\n rooms: spiralState.rooms,\n characters: [...spiralState.characters.values()]\n };\n}\n\nfunction broadcastSpiralState() {\n broadcast(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload()));\n}\n\nfunction ensureRoom(roomId, fallbackName = null) {\n if (!roomId) return spiralState.rooms[0].id;\n const exists = spiralState.rooms.find((r) => r.id === roomId);\n if (exists) return exists.id;\n const created = { id: roomId, name: fallbackName || `Spiral-${roomId.slice(-4)}` };\n spiralState.rooms.push(created);\n return created.id;\n}\n\nfunction syncSpiralCharactersWithWorld(snapshot) {\n const agents = snapshot?.agents || [];\n for (const a of agents) {\n if (!spiralState.characters.has(a.id)) {\n spiralState.characters.set(a.id, {\n id: a.id,\n name: a.id,\n role: \"agent\",\n roomId: spiralState.rooms[0].id,\n source: \"world\"\n });\n }\n }\n}\n\nfunction broadcastExcept(msgObj, exceptSocket) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client !== exceptSocket && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction validateInputFrame(payload, socket) {\n const stats = clientStats.get(socket.clientId) || { lastFrame: 0, windowStart: Date.now(), msgCount: 0 };\n const now = Date.now();\n if (now - stats.windowStart > 5000) {\n stats.windowStart = now;\n stats.msgCount = 0;\n }\n stats.msgCount += 1;\n clientStats.set(socket.clientId, stats);\n if (stats.msgCount > 120) return \"Rate limit exceeded.\";\n\n if (!payload || !Number.isInteger(payload.frame)) return \"frame must be integer.\";\n if (payload.frame <= stats.lastFrame) return \"frame must be monotonic.\";\n const commands = Array.isArray(payload.commands) ? payload.commands : [];\n if (commands.length > 8) return \"Too many commands in one frame.\";\n for (const cmd of commands) {\n if (!cmd || typeof cmd.command !== \"string\") return \"Invalid command format.\";\n if (\n cmd.command !== \"SPAWN_AGENT\" &&\n cmd.command !== \"LIST_CASTLES\" &&\n cmd.command !== \"SPAWN_ENTITY\" &&\n cmd.command !== \"CREATE_CASTLE\"\n ) return \"Unsupported command.\";\n }\n stats.lastFrame = payload.frame;\n clientStats.set(socket.clientId, stats);\n return null;\n}\n\nfunction validateSignalPayload(payload) {\n if (!payload || typeof payload !== \"object\") return \"Invalid signaling payload.\";\n const signalType = payload.signalType;\n if (Object.values(CASTLE_NETWORK_SIGNAL_V0).includes(signalType)) {\n return null;\n }\n if (![\"OFFER\", \"ANSWER\", \"ICE\"].includes(signalType)) return \"Invalid signal type.\";\n if (typeof payload.to !== \"string\" || payload.to.length < 3) return \"Invalid signal destination.\";\n if (signalType !== \"ICE\" && JSON.stringify(payload.sdp || {}).length > 16 * 1024) return \"SDP too large.\";\n if (signalType === \"ICE\" && JSON.stringify(payload.candidate || {}).length > 6 * 1024) return \"ICE too large.\";\n return null;\n}\n\nfunction sanitizeRawMessage(raw) {\n if (!raw) return null;\n const text = raw.toString();\n if (Buffer.byteLength(text, \"utf8\") > MAX_MESSAGE_BYTES) return null;\n return text;\n}\n\nwss.on(\"connection\", async (socket, req) => {\n const origin = req?.headers?.origin || \"\";\n if (!httpCors.isWebSocketOriginAllowed(origin)) {\n socket.close(1008, \"Origin not allowed\");\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const path = req?.url || \"\";\n const query = new URL(`ws://localhost${path}`).searchParams;\n const token = query.get(\"token\");\n if (!token || token !== REQUIRED_GATEWAY_TOKEN) {\n socket.close(1008, \"Invalid gateway token\");\n return;\n }\n }\n if (REQUIRE_AUTH) {\n const result = await verifyClientToken(req);\n if (!result.ok) {\n const isLocal = origin.includes(\"localhost\") || origin.includes(\"127.0.0.1\") || !origin;\n if (!(ALLOW_DEV_ANON && isLocal)) {\n socket.close(1008, result.reason);\n return;\n }\n socket.auth = { ok: true, kind: \"dev-anon\", user: { uid: \"dev-anon\" } };\n } else {\n socket.auth = result;\n }\n }\n\n socket.clientId = `c-${Math.random().toString(36).slice(2, 8)}`;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.HELLO, { message: \"Castle Gateway online.\", clientId: socket.clientId })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.WORLD_SNAPSHOT, orchestrator.worldSnapshot())));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.BROADCAST_STATE, { broadcasterClientId, viewers: [...wss.clients].filter((c) => c.readyState === 1).length })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload())));\n broadcastExcept(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }), socket);\n\n socket.on(\"message\", (raw) => {\n const safeRaw = sanitizeRawMessage(raw);\n if (!safeRaw) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Message too large or invalid.\" })));\n return;\n }\n const parsed = safeJsonParse(safeRaw);\n if (!parsed?.type) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid message.\" })));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND_TEXT) {\n const canonical = parseTextToCommand(parsed.payload?.text);\n if (!canonical) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Unknown command text.\" })));\n return;\n }\n const result = orchestrator.applyCommand(canonical);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND) {\n const result = orchestrator.applyCommand(parsed.payload);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n }\n\n if (parsed.type === WS_MESSAGE.INPUT_FRAME) {\n const err = validateInputFrame(parsed.payload, socket);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const frame = parsed.payload.frame;\n const commands = Array.isArray(parsed.payload?.commands) ? parsed.payload.commands : [];\n orchestrator.queueInputFrame(socket.clientId, frame, commands);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, queued: true, frame })));\n }\n\n if (parsed.type === WS_MESSAGE.SIGNAL) {\n const payload = parsed.payload || {};\n if (Object.values(CASTLE_NETWORK_SIGNAL_V0).includes(payload.signalType)) {\n const netErr = validateCastleNetworkSignalPayloadV0(socket, payload);\n if (netErr) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: netErr })));\n return;\n }\n handleCastleNetworkSignalV0(socket, payload, wss);\n return;\n }\n const err = validateSignalPayload(payload);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const signalType = payload.signalType;\n const to = payload.to;\n for (const client of wss.clients) {\n if (client.clientId === to && client.readyState === 1) {\n client.send(JSON.stringify(createEnvelope(WS_MESSAGE.SIGNAL, { ...payload, from: socket.clientId })));\n break;\n }\n }\n }\n\n if (parsed.type === WS_MESSAGE.DRONE_TELEMETRY) {\n const payload = parsed.payload || {};\n const json = JSON.stringify(payload);\n if (Buffer.byteLength(json, \"utf8\") > 48 * 1024) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"DRONE_TELEMETRY payload too large.\" })));\n return;\n }\n broadcastExcept(createEnvelope(WS_MESSAGE.DRONE_TELEMETRY, { ...payload, relayFrom: socket.clientId }), socket);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_START) {\n handleRhizohVoiceLiveStartV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_CHUNK) {\n handleRhizohVoiceLiveChunkV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_STOP) {\n handleRhizohVoiceLiveStopV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_SOCIAL_PULSE) {\n handleCastleSocialPulse(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_WAL_PEER_FEED) {\n handleCastleWalPeerFeed(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.OPEN_DATA_QUERY) {\n queryOpenData(parsed.payload)\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: true, result })));\n })\n .catch((e) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: false, error: e.message })));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_QUERY) {\n runRhizohBrainV2({\n ...(parsed.payload || {}),\n uid: socket?.auth?.user?.uid || \"unknown\"\n })\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, result)));\n })\n .catch(async () => {\n const fallback = await runRhizohBrain(parsed.payload || {}).catch(() => ({\n ok: false,\n reply: \"Rhizoh temporary disturbance. Habitat remains stable.\"\n }));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, fallback)));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.BROADCAST_REGISTER) {\n const role = String(parsed.payload?.role || \"\");\n if (role === \"GENESIS_BROADCAST_AGENT\") {\n broadcasterClientId = socket.clientId;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, registered: role })));\n broadcastState();\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.STUDIO_CUE) {\n // Only registered broadcaster can push global cues.\n if (!broadcasterClientId || socket.clientId !== broadcasterClientId) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Only broadcaster agent can emit STUDIO_CUE.\" })));\n return;\n }\n const cue = parsed.payload || {};\n if (typeof cue !== \"object\") {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid STUDIO_CUE payload.\" })));\n return;\n }\n broadcast(createEnvelope(WS_MESSAGE.STUDIO_CUE, cue));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_JOIN_ROOM) {\n const requested = String(parsed.payload?.roomId || \"\").trim();\n const requestedName = String(parsed.payload?.roomName || \"\").trim();\n const roomId = ensureRoom(requested || `room-${Math.random().toString(36).slice(2, 7)}`, requestedName || null);\n spiralState.activeByClient.set(socket.clientId, roomId);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, roomId })));\n broadcastSpiralState();\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_CREATE_CHARACTER) {\n const name = String(parsed.payload?.name || \"\").trim().slice(0, 32) || `AI-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;\n const role = String(parsed.payload?.role || \"agent\").trim().slice(0, 24) || \"agent\";\n const roomId = ensureRoom(\n String(parsed.payload?.roomId || spiralState.activeByClient.get(socket.clientId) || spiralState.rooms[0].id),\n null\n );\n\n // Character creation always maps to an authoritative world agent.\n const result = orchestrator.applyCommand({\n command: COMMAND.SPAWN_AGENT,\n params: { targetCastleId: \"castle-01\" }\n });\n if (!result?.ok || !result?.data?.id) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Character creation failed.\" })));\n return;\n }\n const id = result.data.id;\n spiralState.characters.set(id, { id, name, role, roomId, source: \"spawned\" });\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, characterId: id, roomId })));\n broadcastSpiralState();\n return;\n }\n });\n\n socket.on(\"close\", () => {\n if (socket.clientId === broadcasterClientId) broadcasterClientId = null;\n clientStats.delete(socket.clientId);\n rhizohVoiceLiveSessions.delete(socket);\n spiralState.activeByClient.delete(socket.clientId);\n removeCastleSocialClientFromAllRooms(socket.clientId);\n removeCastleNetworkClientV0(socket.clientId, wss);\n broadcast(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }));\n broadcastState();\n });\n});\n\nsetInterval(() => {\n void (async () => {\n try {\n const { frame, snapshot, delta, packedDelta } = orchestrator.tick(1 / 20);\n syncSpiralCharactersWithWorld(snapshot);\n broadcast(createEnvelope(WS_MESSAGE.NETWORK_TICK, { frame }));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_TICK, snapshot));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA, delta));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA_PACKED, packedDelta));\n broadcastSpiralState();\n await runAcademyEventTick();\n const executed = await tickAutomations(Date.now());\n for (const a of executed) {\n await addTranscript(a.uid, {\n source: \"planner\",\n eventType: \"automation-executed\",\n text: `Automation executed: ${a.title} (${a.action})`,\n roomId: \"studio-main\",\n meta: { automationId: a.id, action: a.action, deviceId: a.deviceId || \"\" }\n });\n }\n } catch (e) {\n console.error(\"[GATEWAY] world tick / automations loop error:\", String(e?.message || e));\n }\n })();\n}, 50);\n\n(async () => {\n await rhizohRuntime.telemetry.initOpenTelemetry();\n if (String(process.env.CASTLE_GENESIS_DISK_PERSIST ?? \"\").trim() === \"\") {\n process.env.CASTLE_GENESIS_DISK_PERSIST = \"1\";\n }\n const genesisDiskBoot = await hydrateGenesisContinuityPersistenceBootV0();\n if (!genesisDiskBoot.ok) {\n console.warn(\"[GATEWAY] genesis disk hydrate failed; continuing with in-memory genesis continuity\");\n }\n httpServer.listen(PORT, () => {\n startGenesisCanonicalClock();\n installGenesisCheckpointSurfaceGetter(buildGenesisRuntimeSurfacePayloadLive);\n setGenesisContinuityAfterPublishHook((seq) => {\n noteGenesisCheckpointSeqCommitted(seq);\n noteGenesisContinuitySeqAcceptedV0(seq);\n });\n startGenesisContinuityInfraSampler(buildGenesisRuntimeSurfacePayloadLive, 2000);\n logProductionObservatorySurfaceGuardsV0();\n logGatewaySubstrateAuthorityGuardsV0();\n if (isLlmWorkerEnabledV0()) {\n try {\n const workerBoot = initLlmWorkerAtBootV0();\n console.log(\"[GATEWAY] llm worker boot:\", workerBoot);\n } catch (workerErr) {\n console.error(\"[GATEWAY] llm worker boot failed:\", workerErr);\n }\n }\n console.log(`[GATEWAY] ws/http://localhost:${PORT}`);\n console.log(\n `[GATEWAY] genesis continuity: GET ${rhizohRuntime.routes.genesisRuntime} | POST ${rhizohRuntime.routes.genesisIngress} | SSE ${rhizohRuntime.routes.genesisStream} | replay ${rhizohRuntime.routes.genesisReplay} | diff ${rhizohRuntime.routes.genesisReplayDiff} | equiv ${rhizohRuntime.routes.genesisReplayEquivalence} | analytics ${rhizohRuntime.routes.genesisReplayAnalytics} | evolution ${rhizohRuntime.routes.genesisReplayEvolution}`\n );\n if (String(process.env.CASTLE_GENESIS_OBSERVATORY_LIVE_V01 ?? \"\").trim() === \"1\") {\n console.log(\"GENESIS OBSERVATORY LIVE v0.1 — READ ONLY PHASE STARTED\");\n }\n });\n})().catch((e) => {\n console.error(\"[GATEWAY] fatal boot (before or during listen):\", e);\n process.exit(1);\n});\n"}, "06873f6b5524af4288b0504ee50b223cc8154eb7": {"t": 1785754130.2822554, "n": 175395, "h": "a7662571ffedb3dc", "c": "import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport dotenv from \"dotenv\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconsole.log(\"🔥 [GENESIS_BOOT] entry file:\", __filename);\nconsole.log(\"🔥 [GENESIS_BOOT] node env:\", process.env.NODE_ENV);\nconsole.log(\"🔥 [GENESIS_BOOT] pid:\", process.pid);\nconsole.log(\"🧬 [GENESIS_BOOT] cwd:\", process.cwd());\nconsole.log(\"🧬 [GENESIS_BOOT] import.meta.url:\", import.meta.url);\ndotenv.config({ path: path.join(__dirname, \"..\", \".env\") });\ndotenv.config({ path: path.join(__dirname, \"..\", \".env.local\"), override: true });\n\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport { createServer } from \"node:http\";\nimport { createOrchestrator } from \"@castle/orchestrator\";\nimport { parseTextToCommand } from \"@castle/command-dsl\";\nimport {\n WS_MESSAGE,\n COMMAND,\n createEnvelope,\n createRhizohImmutableEventV0,\n createRhizohPayloadRefV0,\n safeJsonParse,\n validateRhizohVoiceLiveChunkPayloadV0\n} from \"@castle/protocol\";\nimport { queryOpenData } from \"./openData.js\";\nimport { verifyClientToken } from \"./auth.js\";\nimport { runRhizohBrain } from \"./rhizohBrain.js\";\nimport { queryRhizohLlm } from \"./rhizohLlmGateway.js\";\nimport { rhizohGatewayTurn } from \"./rhizohGatewayTurn.js\";\nimport {\n enqueueLlmWorkerTaskAsyncV0,\n getLatestLlmWorkerTaskIdV0,\n getLlmWorkerDebugSnapshotV0,\n getLlmWorkerTaskSnapshotV0,\n initLlmWorkerAtBootV0,\n isLlmWorkerAsyncEnabledV0,\n isLlmWorkerEnabledV0,\n runLlmWorkerTaskV0\n} from \"./llmWorkerRuntime.js\";\nimport {\n RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n runRhizohVoiceTranscribeV3,\n rhizohVoiceTranscribeEnvV3\n} from \"./rhizohVoiceTranscribeV3.js\";\nimport { createHttpCorsPolicy, normalizeHttpOrigin, parseAllowedOriginsFromEnv } from \"./httpCorsPolicyV1.js\";\nimport {\n applyLanguagePropagationToLlmPayloadV1,\n applyLanguagePropagationToVoiceBodyV1,\n buildRhizohLanguagePropagationEchoV1,\n parseRhizohLanguagePropagationV1\n} from \"./rhizohLanguagePropagationV1.js\";\nimport { trimRhizohLlmGatewayPayloadV1 } from \"./rhizohLlmPayloadTrimV1.js\";\nimport {\n meshAppendDelta,\n meshContinuityAggregate,\n meshJoin,\n meshLeave,\n meshReplayFromNodeId,\n meshReplayFromSeq,\n meshSnapshot,\n meshSubscribeSse\n} from \"./presenceRoomMesh.js\";\nimport { runRhizohBrainV2 } from \"./rhizohBrainV2.js\";\nimport {\n listConnections,\n createConnection,\n updateConnection,\n deleteConnection,\n setDefaultConnection,\n resolveConnection\n} from \"./llmConnectionsStore.js\";\nimport { getCloudSyncSnapshotV0, mergeCloudSyncSnapshotV0 } from \"./castleCloudSyncStore.js\";\nimport {\n CASTLE_NETWORK_SIGNAL_V0,\n handleCastleNetworkSignalV0,\n listCastleNetworkPresenceV0,\n removeCastleNetworkClientV0,\n validateCastleNetworkSignalPayloadV0\n} from \"./castleNetworkRelayV0.js\";\nimport { checkHttpRateLimit, getHttpClientIp } from \"./castleHttpRateLimit.js\";\nimport {\n submitAbuseReportV0,\n listModerationQueueV0,\n softBlockUserV0,\n removeSoftBlockV0,\n listSoftBlockedUsersV0\n} from \"./ops/moderationMvpV0.js\";\nimport { listRecentAgentSnapshotsV0 } from \"./ops/agentObservabilityV0.js\";\nimport { readAgentContainmentConfigV0 } from \"./ops/agentContainmentV0.js\";\nimport { readCostContainmentConfigV0, getCostContainmentStatsV0 } from \"./ops/costContainmentV0.js\";\nimport { getGlobalCostLedgerSnapshotV0, getGclBackendStatusV0 } from \"./ops/globalCostLedgerV0.js\";\nimport { evaluateEconomicStrategyL3V0 } from \"./ops/economicStrategyEngineL3V0.js\";\nimport { getPhasedRolloutStatsV0, readPhasedRolloutTierV0 } from \"./ops/phasedRolloutV0.js\";\nimport { buildUnifiedStateNarrativeV0 } from \"./ops/unifiedStateNarrativeV0.js\";\nimport { resolveHardeningTenantIdV0 } from \"./ops/narrativeTenantIsolationV0.js\";\nimport {\n classifyStressResponseV0,\n verifyStressTaxonomyCoverageV0,\n verifyStressConflictResolutionV0\n} from \"./ops/stressResponseTaxonomyV0.js\";\nimport { verifyResolutionStabilityEnvelopeV0 } from \"./ops/resolutionStabilityEnvelopeV0.js\";\nimport { logLlmAccess } from \"./castleLlmAudit.js\";\nimport { appendMemory, listMemories, getMemoryContext, getPersonaGoalMemory, setPersonaGoalMemory, autoCompactMemories } from \"./memoryStore.js\";\nimport { getFirebasePersistence } from \"./firebasePersistence.js\";\nimport { registerAgentIdentity, listAgentIdentities, getAgentIdentity, updateAgentIdentity } from \"./agentIdentityStore.js\";\nimport { queueAcademyEvent, listAcademyEvents, resolveAcademyEvent, runAcademyEventTick } from \"./academyEventEngine.js\";\nimport { enqueuePdfJob, getPdfJob } from \"./pdfPipeline.js\";\nimport {\n createPublishSession,\n listPublishSessions,\n updatePublishSession,\n addTranscript,\n listTranscripts,\n searchMetadata,\n registerDevice,\n listDevices,\n updateDevice,\n appendTelemetry,\n listTelemetry,\n createAutomation,\n listAutomations,\n tickAutomations,\n enqueueCommand,\n listCommands,\n updateCommandStatus,\n registerSocialChannel,\n listSocialChannels\n} from \"./studioOpsStore.js\";\nimport { metrics as infraMetrics } from \"./infra/metrics.js\";\nimport { renderRhizohPrometheusMetrics, rhizohEnterpriseMetrics } from \"./infra/rhizohEnterpriseMetrics.js\";\nimport { scoreHealth } from \"./infra/healthScore.js\";\nimport { getTraceById, listTracesBySession } from \"./infra/traceRegistry.js\";\nimport { canonicalEpistemicSealString, hashAndSignEpistemicSeal, EPISTEMIC_SEAL_SCHEMA } from \"./epistemicSeal.js\";\nimport { persistEpistemicLedgerBatch } from \"./epistemicLedgerStore.js\";\nimport { persistEpistemicForecastBatch } from \"./epistemicForecastStore.js\";\nimport {\n persistAuthorityLedgerWitnessBatchV1,\n getAuthorityLedgerWitnessSnapshotV1,\n replayAuthorityLedgerWitnessChainV1\n} from \"./authorityLedgerWitnessStoreV1.js\";\nimport { buildRhizohExternalGroundTruthPayload } from \"./rhizohExternalGroundTruthGateway.js\";\nimport { ingestRhizohExternalLossBatchHttp } from \"./rhizohExternalLossIngestGateway.js\";\nimport {\n ingestRhizohProductOutcomeHttp,\n listRhizohProductOutcomeAggregates,\n verifyRhizohOutcomeHmac,\n verifyRhizohOutcomeSourceToken\n} from \"./rhizohProductOutcomeIngestGateway.js\";\nimport { initRhizoh } from \"./rhizohProductionBootstrap.js\";\nimport { handleAcademicObservatoryExportGetV0 } from \"./rhizoh/academicObservatoryHttpV0.js\";\nimport { handleCouncilAnomalyReasoningPostV0 } from \"./council/rhizohEpistemicCouncilHttpV0.js\";\nimport {\n handleLiveSportsBundleGetV0,\n handleLiveNewsHeadlinesGetV0,\n handleLiveWorldFeedGetV0\n} from \"./rhizoh/liveWorldFeedHttpV0.js\";\nimport {\n buildGenesisRuntimeSurfacePayload,\n recordGenesisEpistemicLedgerPersisted,\n recordGenesisEpistemicSealIssued,\n startGenesisCanonicalClock\n} from \"./genesisRuntimeSurfaceV0.js\";\nimport { registerGenesisSseClient, getGenesisContinuitySeq, setGenesisContinuityAfterPublishHook } from \"./genesisContinuityStreamHubV0.js\";\nimport { computeGenesisReplayFingerprintV0 } from \"./genesisReplayFingerprintV0.js\";\nimport { buildGenesisGatewayCapabilitiesV0 } from \"./genesisGatewayCapabilitiesV0.js\";\nimport {\n installGenesisCheckpointSurfaceGetter,\n noteGenesisCheckpointSeqCommitted,\n getLatestGenesisSignedCheckpoint\n} from \"./genesisContinuityCheckpointV0.js\";\nimport { hydrateGenesisContinuityPersistenceBootV0 } from \"./genesisContinuityHydrateBootV0.js\";\nimport { startGenesisContinuityInfraSampler } from \"./genesisContinuityInfraSamplerV0.js\";\nimport {\n genesisCheckpointQueryBySeqV0,\n genesisCheckpointQueryRangeV0,\n genesisCheckpointQueryLineageV0\n} from \"./genesisCheckpointQueryV0.js\";\nimport {\n genesisContinuityEventArchiveEnabled,\n queryGenesisContinuityEventArchiveV0,\n GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION\n} from \"./genesisContinuityEventArchiveV0.js\";\nimport { resolveGenesisReplayRouterV1, GENESIS_REPLAY_ROUTER_SCHEMA } from \"./genesisReplayRouterV1.js\";\nimport {\n computeGenesisReplayTemporalDiffV1,\n GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA\n} from \"./genesisReplayTemporalDiffV1.js\";\nimport { compareGenesisReplayEquivalenceV1 } from \"./genesisReplayEquivalenceV1.js\";\nimport { computeGenesisReplayAnalyticsV1 } from \"./genesisReplayAnalyticsV1.js\";\nimport { computeGenesisReplayEvolutionV1 } from \"./genesisReplayEvolutionV1.js\";\nimport { handleStripeCheckoutCreate, handleStripeMembershipWebhook } from \"./stripeWebhookMembership.js\";\nimport { buildRealityHealthPayload } from \"./edgeRealityConsistencyV1.js\";\nimport {\n assertWalPeerSocketAuthorityV0,\n buildGatewaySubstrateAuthoritySnapshotV0,\n logGatewaySubstrateAuthorityGuardsV0,\n validateWalPeerFeedSignatureV0\n} from \"./gatewaySubstrateAuthorityV0.js\";\nimport {\n buildSubstrateOperationalSnapshotV0,\n recordWalPeerRejectV0,\n renderSubstratePrometheusMetricsV0\n} from \"./infra/substrateOperationalMetrics.js\";\nimport { ingestClientSubstrateHealthV0 } from \"./rhizohSubstrateHealthIngestV0.js\";\nimport { recordSubstrateOtelEventV0 } from \"./infra/opentelemetrySubstrateV0.js\";\nimport {\n GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n getGenesisWorldObservationIngressStatusV0,\n ingestGenesisWorldObservationV0\n} from \"./genesisContinuityClientIngressV0.js\";\nimport {\n getGenesisContinuitySeqAuditV0,\n noteGenesisContinuitySeqAcceptedV0\n} from \"./genesisContinuitySeqGapV0.js\";\n\n/** Render / Fly / Railway: `PORT` — yoksa `CASTLE_GATEWAY_PORT` — yoksa 8090. */\nconst PORT =\n Number(process.env.PORT) ||\n Number(process.env.CASTLE_GATEWAY_PORT) ||\n 8090;\n/** Spiral / world WebSocket server — set after `wss` init; used by genesis runtime surface. */\nlet spiralWssForGenesis = null;\nconst MAX_MESSAGE_BYTES = Number(process.env.CASTLE_MAX_MESSAGE_BYTES || 32 * 1024);\nconst REQUIRED_GATEWAY_TOKEN = process.env.CASTLE_GATEWAY_TOKEN || \"\";\nconst ALLOWED_ORIGINS = parseAllowedOriginsFromEnv().fromList;\nconst httpCors = createHttpCorsPolicy();\n\n/** @returns {string|null} `Access-Control-Allow-Origin`; null = browser CORS fail. */\nfunction accessControlAllowOriginValue(req) {\n return httpCors.accessControlAllowOriginValue(req);\n}\n\nfunction applyHttpCorsHeaders(req, res) {\n return httpCors.applyHttpCorsHeaders(req, res);\n}\nconst REQUIRE_AUTH = process.env.CASTLE_REQUIRE_AUTH === \"true\";\nconst ALLOW_DEV_ANON = process.env.CASTLE_ALLOW_DEV_ANON !== \"false\";\nconst ALLOW_DEV_HTTP_UID = process.env.CASTLE_ALLOW_DEV_HTTP_UID !== \"false\";\n/** Presence mesh: allow `X-Castle-Guest-Id` when Firebase auth absent (default on). Set `false` to require auth only. */\nconst ALLOW_MESH_GUEST = process.env.CASTLE_ALLOW_MESH_GUEST !== \"false\";\nconst SOCIAL_RETRY_MAX = Math.max(0, Math.min(5, Number(process.env.CASTLE_SOCIAL_RETRY_MAX || 2)));\nconst SOCIAL_RETRY_BASE_MS = Math.max(100, Number(process.env.CASTLE_SOCIAL_RETRY_BASE_MS || 700));\nconst TELEGRAM_BOT_TOKEN = process.env.CASTLE_TELEGRAM_BOT_TOKEN || \"\";\nconst WHATSAPP_TOKEN = process.env.CASTLE_WHATSAPP_TOKEN || \"\";\nconst WHATSAPP_PHONE_NUMBER_ID = process.env.CASTLE_WHATSAPP_PHONE_NUMBER_ID || \"\";\nconst RL_RHIZOH_LLM_PER_MIN = Math.max(5, Number(process.env.CASTLE_RL_RHIZOH_LLM_PER_MIN || 40));\nconst RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN || 30)\n);\nconst RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN || 120));\nconst RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN = Math.max(\n 10,\n Number(process.env.CASTLE_RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN || 60)\n);\nconst RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN = Math.max(8, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN || 48));\nconst RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN = Math.max(4, Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN || 24));\nconst RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN || 18)\n);\nconst RL_EPISTEMIC_SEAL_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_EPISTEMIC_SEAL_PER_MIN || 120));\nconst RL_EPISTEMIC_LOG_PER_MIN = Math.max(20, Number(process.env.CASTLE_RL_EPISTEMIC_LOG_PER_MIN || 240));\nconst EPISTEMIC_SEAL_SECRET = String(\n process.env.CASTLE_EPISTEMIC_SEAL_SECRET || process.env.CASTLE_GATEWAY_TOKEN || \"\"\n).trim();\nconst RL_LLM_CONN_TEST_PER_MIN = Math.max(2, Number(process.env.CASTLE_RL_LLM_CONNECTION_TEST_PER_MIN || 8));\n\n/** One-shot production warnings: CORS surface, auth toggle, dev bypass flags (observatory deploy hygiene). */\nfunction logProductionObservatorySurfaceGuardsV0() {\n if (String(process.env.NODE_ENV || \"\").toLowerCase() !== \"production\") return;\n const tag = \"[GATEWAY_PROD_GUARD]\";\n const primaryCors = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n const corsListEmpty = ALLOWED_ORIGINS.length === 0;\n if (corsListEmpty && !primaryCors) {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty and CASTLE_HTTP_CORS_ORIGIN empty — production still merges RHIZOH_BROWSER_ORIGINS_V1 defaults; set env for preview/staging hosts`\n );\n } else if (corsListEmpty && primaryCors && primaryCors !== \"*\") {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty — only CASTLE_HTTP_CORS_ORIGIN set; add web.app + firebaseapp.com (and previews) to CASTLE_ALLOWED_ORIGINS if clients use alternate hostnames`\n );\n }\n if (primaryCors && /localhost|127\\.0\\.0\\.1/i.test(primaryCors)) {\n console.warn(`${tag} CORS: CASTLE_HTTP_CORS_ORIGIN is localhost while NODE_ENV=production`);\n }\n if (REQUIRE_AUTH) {\n console.warn(\n `${tag} AUTH: CASTLE_REQUIRE_AUTH=true — clients must send a valid gateway credential (otherwise silent 401 on replay/SSE); EventSource cannot set custom headers unless proxied`\n );\n if (REQUIRED_GATEWAY_TOKEN.length < 16) {\n console.warn(`${tag} AUTH: CASTLE_GATEWAY_TOKEN missing or short while require-auth enabled`);\n }\n }\n if (ALLOW_DEV_ANON) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_ANON is not \"false\" — dev anonymous path may be live in production (set CASTLE_ALLOW_DEV_ANON=false)`\n );\n }\n if (ALLOW_DEV_HTTP_UID) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_HTTP_UID is not \"false\" — X-Castle-Dev-Uid bypass risk (set CASTLE_ALLOW_DEV_HTTP_UID=false)`\n );\n }\n}\n\nconst rhizohRuntime = initRhizoh();\n\n/** İstemci: llmKeySource veya keySource — env | user_connection | auto */\nfunction normalizeClientLlmKeySource(raw) {\n const s = String(raw ?? \"\").trim().toLowerCase();\n if (s === \"env\" || s === \"server\") return \"env\";\n if (s === \"user_connection\" || s === \"connection\" || s === \"user\") return \"user_connection\";\n if (s === \"auto\" || s === \"\") return \"auto\";\n return null;\n}\n\n/**\n * Server-side provider wins for env/auto unless user_connection — client bundle may still send provider=openai.\n * @param {\"env\"|\"user_connection\"|\"auto\"} keyMode\n * @param {{ provider?: string, model?: string, apiKey?: string } | null} conn\n * @param {{ provider?: string, model?: string }} payload\n */\nfunction resolveServerLlmProviderV0(keyMode, conn, payload) {\n const envProvider = String(process.env.CASTLE_LLM_PROVIDER || \"\").trim() || undefined;\n const envModel = String(process.env.CASTLE_LLM_MODEL || \"\").trim() || undefined;\n\n if (keyMode === \"user_connection\") {\n return {\n resolvedProvider: conn?.provider || payload?.provider,\n resolvedModel: conn?.model || payload?.model,\n connApiKey: conn?.apiKey || \"\"\n };\n }\n\n if (keyMode === \"env\") {\n return {\n resolvedProvider: envProvider ?? payload?.provider,\n resolvedModel: envModel ?? payload?.model,\n connApiKey: \"\"\n };\n }\n\n if (conn?.provider && conn?.apiKey) {\n return {\n resolvedProvider: conn.provider,\n resolvedModel: conn.model,\n connApiKey: conn.apiKey\n };\n }\n\n return {\n resolvedProvider: envProvider ?? payload?.provider,\n resolvedModel: envModel ?? payload?.model,\n connApiKey: \"\"\n };\n}\n\nfunction jsonReplacerSafeV0(_key, value) {\n if (typeof value === \"bigint\") return Number(value);\n return value;\n}\n\nfunction sendJson(res, statusCode, payload) {\n res.writeHead(statusCode, { \"Content-Type\": \"application/json; charset=utf-8\" });\n try {\n res.end(JSON.stringify(payload, jsonReplacerSafeV0));\n } catch (error) {\n res.end(\n JSON.stringify({\n ok: false,\n error: \"response_serialize_failed\",\n detail: String(error?.message || error)\n })\n );\n }\n}\n\nfunction logRhizohHealth(stage, detail = {}) {\n try {\n console.info(`[CASTLE_${String(stage || \"unknown\")}]`, detail && typeof detail === \"object\" ? detail : {});\n } catch {\n /* noop */\n }\n}\n\nfunction renderInfraMetricsProm() {\n const lines = [\n `castle_gateway_events_processed_total ${infraMetrics.eventsProcessed}`,\n `castle_gateway_duplicate_reject_total ${infraMetrics.duplicateRejects}`,\n `castle_gateway_enqueue_latency_ms ${infraMetrics.enqueueLatencyMs}`,\n `castle_gateway_queue_depth ${infraMetrics.queueDepth}`,\n `castle_gateway_queue_lag_seconds ${(Number(infraMetrics.queueLag || 0) / 1000).toFixed(3)}`,\n `castle_gateway_errors_total ${infraMetrics.errors}`\n ];\n const buckets = infraMetrics.enqueueLatencyBuckets || {};\n for (const k of Object.keys(buckets)) {\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"${k}\"} ${buckets[k]}`);\n }\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"+Inf\"} ${infraMetrics.eventsProcessed}`);\n if (process.env.CASTLE_SUBSTRATE_METRICS !== \"0\") {\n lines.push(renderSubstratePrometheusMetricsV0().trimEnd());\n }\n return lines.join(\"\\n\");\n}\n\nasync function fetchJsonWithTimeout(url, timeoutMs = 1200) {\n const ctl = new AbortController();\n const tid = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const res = await fetch(url, { signal: ctl.signal });\n if (!res.ok) return null;\n return await res.json();\n } catch {\n return null;\n } finally {\n clearTimeout(tid);\n }\n}\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction getHttpPathname(req) {\n let p = String(req.url || \"\").split(\"?\")[0];\n /* Reverse proxies / clients sometimes append `/` — avoid silent 404 on e.g. `/rhizoh/genesis/stream/`. */\n while (p.length > 1 && p.endsWith(\"/\")) {\n p = p.slice(0, -1);\n }\n return p;\n}\n\nfunction rhizohLlmEnvConfigured() {\n const p = String(process.env.CASTLE_LLM_PROVIDER || \"openai\").toLowerCase();\n const key =\n p === \"anthropic\"\n ? process.env.ANTHROPIC_API_KEY\n : p === \"gemini\"\n ? process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY\n : p === \"xai\"\n ? process.env.XAI_API_KEY\n : p === \"deepseek\"\n ? process.env.DEEPSEEK_API_KEY\n : p === \"mistral\"\n ? process.env.MISTRAL_API_KEY\n : p === \"openrouter\"\n ? process.env.OPENROUTER_API_KEY\n : process.env.OPENAI_API_KEY;\n return !!String(key || \"\").trim();\n}\n\n/**\n * @param {import(\"firebase-admin/firestore\").Firestore | null} db\n * @param {number} timeoutMs\n */\nasync function probeFirestoreReachable(db, timeoutMs = 2000) {\n if (!db) return { ok: false, reason: \"no_db\" };\n const timeout = sleep(timeoutMs).then(() => ({ ok: false, reason: \"timeout\" }));\n const work = db\n .collection(\"_castle_gateway_health\")\n .doc(\"ping\")\n .get()\n .then(() => ({ ok: true, reason: \"reachable\" }))\n .catch((e) => ({ ok: false, reason: String(e?.message || e || \"firestore_error\") }));\n return Promise.race([work, timeout]);\n}\n\nfunction stripHtml(text = \"\") {\n return String(text)\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \" \")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \" \")\n .replace(/<[^>]+>/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction haversineMeters(aLat, aLon, bLat, bLon) {\n const R = 6371000;\n const dLat = ((bLat - aLat) * Math.PI) / 180;\n const dLon = ((bLon - aLon) * Math.PI) / 180;\n const sa = Math.sin(dLat / 2);\n const sb = Math.sin(dLon / 2);\n const x = sa * sa + Math.cos((aLat * Math.PI) / 180) * Math.cos((bLat * Math.PI) / 180) * sb * sb;\n return 2 * R * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));\n}\n\nasync function checkPublishTargetHealth(protocol, target) {\n const p = String(protocol || \"\").toUpperCase();\n const t = String(target || \"\").trim();\n if (!t) return { ok: false, reason: \"target_missing\", shouldReconnect: false };\n if (p === \"RTMP\") {\n const ok = t.startsWith(\"rtmp://\") || t.startsWith(\"rtmps://\");\n return { ok, reason: ok ? \"rtmp_target_format_ok\" : \"invalid_rtmp_target\", shouldReconnect: !ok, reconnectInSec: 15 };\n }\n if (p === \"WHIP\" || p === \"SFU\") {\n try {\n const res = await fetch(t, { method: \"OPTIONS\" });\n const ok = res.status < 500;\n return { ok, reason: `http_${res.status}`, shouldReconnect: !ok, reconnectInSec: 8 };\n } catch {\n return { ok: false, reason: \"network_unreachable\", shouldReconnect: true, reconnectInSec: 8 };\n }\n }\n return { ok: true, reason: \"unchecked_protocol\", shouldReconnect: false };\n}\n\nasync function fetchTextFromUrl(url) {\n const res = await fetch(url, { method: \"GET\" });\n if (!res.ok) throw new Error(`fetch_${res.status}`);\n const ct = String(res.headers.get(\"content-type\") || \"\");\n if (ct.includes(\"application/pdf\")) throw new Error(\"pdf_binary_not_supported_directly\");\n const body = await res.text();\n return stripHtml(body).slice(0, 14000);\n}\n\nfunction buildTaskActionGraph(task = \"\", context = {}) {\n const t = String(task || \"\").toLowerCase();\n if (t.includes(\"dinner\") || t.includes(\"akşam yemeği\") || t.includes(\"aksam yemegi\")) {\n return {\n taskTitle: \"Dinner Prep Planner\",\n nodes: [\n { id: \"n1\", title: \"Kitchen preheat\", action: \"smart-oven.preheat\", params: { temperatureC: 190, mode: \"fan\" } },\n { id: \"n2\", title: \"Lighting comfort\", action: \"smart-light.set_scene\", params: { scene: \"warm_dinner\" } },\n { id: \"n3\", title: \"Fridge inventory check\", action: \"smart-fridge.inventory_scan\", params: {} },\n { id: \"n4\", title: \"Reminder notification\", action: \"smart-watch.notify\", params: { text: \"Dinner prep started.\" } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" },\n { from: \"n3\", to: \"n4\" }\n ]\n };\n }\n return {\n taskTitle: task || \"General Planner\",\n nodes: [\n { id: \"n1\", title: \"Safety check\", action: \"device.safety_check\", params: { context } },\n { id: \"n2\", title: \"Primary action\", action: \"device.execute_task\", params: { task } },\n { id: \"n3\", title: \"Report\", action: \"studio.timeline_note\", params: { task } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" }\n ]\n };\n}\n\nfunction buildEthicsPrograms() {\n return {\n policyVersion: \"v1\",\n tracks: [\n {\n id: \"elderly-care-assist\",\n audience: \"elderly\",\n title: \"Elderly Assistance Track\",\n principles: [\"human-in-the-loop\", \"consent-first\", \"low-friction alerts\"],\n modules: [\"medication-reminder\", \"fall-risk-check\", \"family-notify\", \"daily-companion-brief\"]\n },\n {\n id: \"kids-safe-learning\",\n audience: \"children\",\n title: \"Kids Safe Learning Track\",\n principles: [\"guardian-consent\", \"age-appropriate-language\", \"content-filtering\"],\n modules: [\"study-coach\", \"screen-time-balance\", \"safe-social-share\", \"emergency-contact\"]\n }\n ]\n };\n}\n\nasync function deliverViaTelegram(channel, text) {\n const chatId = String(channel.endpoint || \"\").trim();\n if (!TELEGRAM_BOT_TOKEN) throw new Error(\"telegram_token_missing\");\n if (!chatId) throw new Error(\"telegram_chat_id_missing\");\n const url = `https://api.telegram.org/bot${encodeURIComponent(TELEGRAM_BOT_TOKEN)}/sendMessage`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ chat_id: chatId, text })\n });\n if (!res.ok) throw new Error(`telegram_http_${res.status}`);\n return { provider: \"telegram\", status: res.status };\n}\n\nasync function deliverViaWhatsapp(channel, text) {\n const to = String(channel.endpoint || \"\").trim();\n if (!WHATSAPP_TOKEN) throw new Error(\"whatsapp_token_missing\");\n if (!WHATSAPP_PHONE_NUMBER_ID) throw new Error(\"whatsapp_phone_number_id_missing\");\n if (!to) throw new Error(\"whatsapp_to_missing\");\n const url = `https://graph.facebook.com/v22.0/${encodeURIComponent(WHATSAPP_PHONE_NUMBER_ID)}/messages`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${WHATSAPP_TOKEN}` },\n body: JSON.stringify({ messaging_product: \"whatsapp\", to, type: \"text\", text: { body: text } })\n });\n if (!res.ok) throw new Error(`whatsapp_http_${res.status}`);\n return { provider: \"whatsapp\", status: res.status };\n}\n\nasync function deliverViaWebhook(channel, text) {\n const endpoint = String(channel.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"webhook_endpoint_missing\");\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text, platform: channel.platform || \"webhook\", ts: Date.now() })\n });\n if (!res.ok) throw new Error(`webhook_http_${res.status}`);\n return { provider: \"webhook\", status: res.status };\n}\n\nasync function deliverSocialMessage(channel, text) {\n const platform = String(channel.platform || \"webhook\").toLowerCase();\n if (platform === \"telegram\") return deliverViaTelegram(channel, text);\n if (platform === \"whatsapp\") return deliverViaWhatsapp(channel, text);\n return deliverViaWebhook(channel, text);\n}\n\nasync function deliverSocialWithRetry(channel, text) {\n let attempt = 0;\n let lastError = null;\n while (attempt <= SOCIAL_RETRY_MAX) {\n try {\n const result = await deliverSocialMessage(channel, text);\n return { ok: true, attempt: attempt + 1, ...result };\n } catch (error) {\n lastError = error;\n if (attempt >= SOCIAL_RETRY_MAX) break;\n const jitter = Math.floor(Math.random() * 90);\n await sleep(SOCIAL_RETRY_BASE_MS * Math.pow(2, attempt) + jitter);\n }\n attempt += 1;\n }\n return { ok: false, attempt: attempt + 1, error: String(lastError?.message || \"delivery_failed\") };\n}\n\nasync function dispatchCommandToAdapter(device, command) {\n const adapter = String(command?.adapter || device?.adapter || \"websocket\").toLowerCase();\n const endpoint = String(device?.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"device_endpoint_missing\");\n if (adapter === \"mqtt\") {\n // MQTT delivery is proxied via a bridge endpoint (HTTP webhook style).\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n type: \"mqtt-command\",\n topic: `castle/devices/${device.id}/commands`,\n action: command.action,\n params: command.params || {}\n })\n });\n if (!res.ok) throw new Error(`mqtt_bridge_http_${res.status}`);\n return { ok: true, adapter, via: \"http-bridge\", status: res.status };\n }\n if (!endpoint.startsWith(\"ws://\") && !endpoint.startsWith(\"wss://\")) throw new Error(\"invalid_websocket_endpoint\");\n const sendResult = await new Promise((resolve, reject) => {\n const ws = new WebSocket(endpoint);\n const timer = setTimeout(() => {\n try {\n ws.close();\n } catch {\n /* noop */\n }\n reject(new Error(\"websocket_timeout\"));\n }, 3000);\n ws.on(\"open\", () => {\n ws.send(\n JSON.stringify({\n type: \"castle-command\",\n commandId: command.id,\n deviceId: command.deviceId,\n action: command.action,\n params: command.params || {}\n }),\n (err) => {\n clearTimeout(timer);\n ws.close();\n if (err) reject(new Error(\"websocket_send_failed\"));\n else resolve({ ok: true, adapter: \"websocket\", via: \"ws\" });\n }\n );\n });\n ws.on(\"error\", () => {\n clearTimeout(timer);\n reject(new Error(\"websocket_connect_failed\"));\n });\n });\n return sendResult;\n}\n\nfunction readHttpJson(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => {\n if (!buf) return resolve({});\n const payload = safeJsonParse(buf);\n if (!payload || typeof payload !== \"object\") return reject(new Error(\"invalid_json_payload\"));\n resolve(payload);\n });\n req.on(\"error\", reject);\n });\n}\n\n/** Ham gövde (HMAC doğrulaması için). */\nfunction readHttpBodyText(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => resolve(buf));\n req.on(\"error\", reject);\n });\n}\n\nfunction maskOpaqueId(value) {\n const t = String(value || \"\");\n if (!t) return \"(empty)\";\n if (t.length <= 8) return \"****\";\n return `${t.slice(0, 4)}…${t.slice(-2)}`;\n}\n\nfunction readGatewayHttpToken(req) {\n const x = String(req.headers?.[\"x-castle-gateway-token\"] || \"\").trim();\n if (x) return x;\n const auth = String(req.headers?.authorization || \"\");\n const m = /^Bearer\\s+(.+)$/i.exec(auth);\n return m ? String(m[1] || \"\").trim() : \"\";\n}\n\nasync function resolveHttpUser(req) {\n const authResult = await verifyClientToken(req);\n if (authResult?.ok && authResult.user?.uid) {\n return { ok: true, uid: String(authResult.user.uid), mode: authResult.kind || \"auth\" };\n }\n if (ALLOW_DEV_ANON && ALLOW_DEV_HTTP_UID) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) return { ok: true, uid: `dev-${devUid.slice(0, 64)}`, mode: \"dev-header\" };\n }\n return { ok: false, reason: authResult?.reason || \"auth_required\" };\n}\n\n/**\n * Epistemic ledger ingest — Firebase uid OR valid transport token + X-Castle-Dev-Uid.\n * Prod disables CASTLE_ALLOW_DEV_HTTP_UID; LLM may still work while telemetry 401'd without this.\n */\nasync function resolveEpistemicIngestActor(req) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok && tok === REQUIRED_GATEWAY_TOKEN) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) {\n return { ok: true, uid: `gw-telemetry-${devUid.slice(0, 64)}`, mode: \"gateway-token-dev-uid\" };\n }\n return { ok: false, reason: \"dev_uid_required\" };\n }\n if (tok) {\n return { ok: false, reason: \"gateway_token_mismatch\" };\n }\n }\n const auth = await resolveHttpUser(req);\n if (auth.ok) return auth;\n return { ok: false, reason: auth.reason || \"auth_required\" };\n}\n\n/** Mesh actor: Firebase uid, dev header, or `guest:<uuid>` via header / `guestId` query (SSE cannot set headers). */\nasync function resolveMeshActor(req) {\n const auth = await resolveHttpUser(req);\n if (auth.ok) return { ok: true, clientUid: auth.uid, mode: auth.mode };\n if (ALLOW_MESH_GUEST) {\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const gid = String(req.headers?.[\"x-castle-guest-id\"] || q.searchParams.get(\"guestId\") || \"\").trim();\n if (gid) {\n const normalized = gid.startsWith(\"guest:\") ? gid.slice(0, 180) : `guest:${gid.slice(0, 160)}`;\n return { ok: true, clientUid: normalized, mode: \"guest-header\" };\n }\n }\n return { ok: false, reason: \"mesh_auth_required\" };\n}\n\n/** Live genesis runtime JSON (GET /rhizoh/genesis/runtime + SSE infra sampler). */\nasync function buildGenesisRuntimeSurfacePayloadLive() {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n let spiralWsActive = 0;\n if (spiralWssForGenesis) {\n for (const c of spiralWssForGenesis.clients) {\n if (c.readyState === 1) spiralWsActive += 1;\n }\n }\n const base = buildGenesisRuntimeSurfacePayload({\n infraMetrics,\n scoredHealth: scored,\n rhizohEnterpriseMetrics,\n mesh: meshContinuityAggregate(),\n workerHealth: worker,\n port: PORT,\n spiralWebSocketClientsActive: spiralWsActive\n });\n return {\n ...base,\n replayFingerprint: computeGenesisReplayFingerprintV0(base),\n gatewayCapabilities: buildGenesisGatewayCapabilitiesV0(),\n genesisStream: {\n schema: \"castle.genesis.stream_cursor.v0\",\n lastAcceptedSeq: getGenesisContinuitySeq(),\n clientIngress: {\n schema: GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n contract: \"castle.world_observation.ingress_envelope.v1\",\n postPath: rhizohRuntime.routes.genesisIngress,\n closure: getGenesisWorldObservationIngressStatusV0()\n },\n seqContinuity: getGenesisContinuitySeqAuditV0(),\n eventArchive: genesisContinuityEventArchiveEnabled()\n ? {\n enabled: true,\n projection: GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION,\n queryPath: rhizohRuntime.routes.genesisContinuityEvents\n }\n : { enabled: false },\n replayRouter: {\n schema: GENESIS_REPLAY_ROUTER_SCHEMA,\n queryPath: rhizohRuntime.routes.genesisReplay,\n queryHint: \"from=&to= or range=from-to; optional type=&checkpoints=0\",\n temporalDiffPath: rhizohRuntime.routes.genesisReplayDiff,\n temporalDiffSchema: GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA,\n equivalencePath: rhizohRuntime.routes.genesisReplayEquivalence,\n analyticsPath: rhizohRuntime.routes.genesisReplayAnalytics,\n evolutionPath: rhizohRuntime.routes.genesisReplayEvolution\n }\n }\n };\n}\n\nconst httpServer = createServer(async (req, res) => {\n const corsAllow = applyHttpCorsHeaders(req, res);\n if (req.method === \"OPTIONS\") {\n if (!corsAllow && req.headers?.origin) {\n console.warn(\"[GATEWAY_CORS] preflight rejected for origin:\", req.headers.origin);\n }\n res.writeHead(corsAllow ? 204 : 403);\n res.end();\n return;\n }\n\n const pathname = getHttpPathname(req);\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/genesis/__ping\") {\n sendJson(res, 200, {\n ok: true,\n pid: process.pid,\n entry: __filename,\n time: Date.now(),\n genesis: true\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/epistemic/__probe\") {\n sendJson(res, 200, {\n ok: true,\n epistemicIngestActorV2: true,\n routes: {\n seal: rhizohRuntime.routes.epistemicSeal,\n logsBatch: rhizohRuntime.routes.epistemicLogsBatch,\n authorityLedgerBatch: rhizohRuntime.routes.authorityLedgerBatch,\n authorityLedgerReplay: rhizohRuntime.routes.authorityLedgerReplay\n },\n hasGatewayToken: Boolean(REQUIRED_GATEWAY_TOKEN),\n gatewayTokenLen: REQUIRED_GATEWAY_TOKEN ? REQUIRED_GATEWAY_TOKEN.length : 0\n });\n return;\n }\n\n if (\n req.method === \"GET\" &&\n (pathname === \"/rhizoh/live/world-feed\" ||\n pathname === \"/rhizoh/live/sports-bundle\" ||\n pathname === \"/rhizoh/live/news-headlines\")\n ) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_live_world_feed:${ip}`, RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const out =\n pathname === \"/rhizoh/live/sports-bundle\"\n ? await handleLiveSportsBundleGetV0()\n : pathname === \"/rhizoh/live/news-headlines\"\n ? await handleLiveNewsHeadlinesGetV0(req)\n : await handleLiveWorldFeedGetV0(req);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/webhooks/stripe\") {\n await handleStripeMembershipWebhook(req, res);\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/billing/stripe/checkout\") {\n await handleStripeCheckoutCreate(req, res, { readHttpJson, resolveHttpUser, sendJson });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/live\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, live: false, dns: true, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n sendJson(res, 200, { ok: true, live: true, dns: true, service: \"castle-gateway\", ts: Date.now() });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/cors\") {\n const reqOrigin = normalizeHttpOrigin(req.headers?.origin);\n const allowOrigin = accessControlAllowOriginValue(req);\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n requestOrigin: reqOrigin || null,\n allowed: Boolean(allowOrigin),\n allowOrigin: allowOrigin || null,\n allowSetSize: httpCors.allowSet.size\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/ready\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, ready: false, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n try {\n const persistence = getFirebasePersistence();\n const llmProbe = rhizohLlmEnvConfigured();\n sendJson(res, 200, {\n ok: true,\n ready: true,\n dns: true,\n persistence: persistence.mode,\n service: \"castle-gateway\",\n gateway: \"ok\",\n llm: llmProbe ? \"ok\" : \"degraded\",\n spine: llmProbe ? \"ok\" : \"partial\"\n });\n } catch (e) {\n sendJson(res, 503, { ok: false, ready: false, reason: String(e?.message || e), service: \"castle-gateway\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/reality\") {\n const payload = buildRealityHealthPayload();\n sendJson(res, payload.ok ? 200 : 503, payload);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/substrate\") {\n const authority = buildGatewaySubstrateAuthoritySnapshotV0();\n const operational = buildSubstrateOperationalSnapshotV0();\n const drift = buildRealityHealthPayload();\n const ok = Boolean(authority.productionAuthoritySatisfied) && drift.ok;\n sendJson(res, ok ? 200 : 503, {\n ok,\n service: \"castle-gateway\",\n ts: Date.now(),\n authority,\n operational,\n drift: { ok: drift.ok, errors: drift.drift?.errors, warnings: drift.drift?.warnings }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.councilAnomalyReasoning) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_council_anomaly:${ip}`, 30, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const body = await readHttpJson(req);\n const out = handleCouncilAnomalyReasoningPostV0(body);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/substrate/health\") {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const body = await readHttpJson(req);\n const uid = (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestClientSubstrateHealthV0(body, { uid, clientId: uid });\n if (!result.ok) {\n sendJson(res, 400, result);\n return;\n }\n recordSubstrateOtelEventV0(\"client_health_ingest\", { uid: String(uid).slice(0, 32) });\n sendJson(res, 200, result);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/deps\") {\n const t0 = Date.now();\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, {\n ok: false,\n dns: true,\n llm: false,\n firestore: false,\n phase: \"maintenance\",\n latencyMs: Date.now() - t0,\n service: \"castle-gateway\"\n });\n return;\n }\n const persistence = getFirebasePersistence();\n let firestoreOk = persistence.mode !== \"firebase\";\n let firestoreDetail = persistence.mode === \"file\" ? \"file_mode\" : \"not_applicable\";\n if (persistence.mode === \"firebase\" && persistence.db) {\n const pr = await probeFirestoreReachable(persistence.db, 2000);\n firestoreOk = pr.ok;\n firestoreDetail = pr.reason || (pr.ok ? \"reachable\" : \"fail\");\n } else if (persistence.mode === \"firebase\" && !persistence.db) {\n firestoreOk = false;\n firestoreDetail = \"firebase_not_initialized\";\n }\n const llmOk = rhizohLlmEnvConfigured();\n const overallOk = llmOk && firestoreOk;\n const latencyMs = Date.now() - t0;\n sendJson(res, 200, {\n ok: overallOk,\n dns: true,\n llm: llmOk,\n firestore: persistence.mode === \"firebase\" ? firestoreOk : false,\n persistence: persistence.mode,\n firestoreDetail,\n latencyMs,\n service: \"castle-gateway\",\n wsPort: PORT\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health\") {\n const persistence = getFirebasePersistence();\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n wsPort: PORT,\n persistence: persistence.mode,\n presenceMesh: \"sse\"\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/health\") {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n sendJson(res, 200, {\n ok: true,\n role: \"gateway\",\n queueLagMs: infraMetrics.queueLag,\n queueDepth: infraMetrics.queueDepth,\n errors: infraMetrics.errors,\n status: scored.status,\n score: scored.score,\n reasons: scored.reasons,\n worker: worker\n ? {\n status: worker.status,\n score: worker.score,\n reasons: worker.reasons\n }\n : null,\n timestamp: Date.now()\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisRuntime) {\n const payload = await buildGenesisRuntimeSurfacePayloadLive();\n sendJson(res, 200, payload);\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.genesisIngress) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const actor = await resolveMeshActor(req);\n const clientId = actor.ok ? actor.clientUid : (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestGenesisWorldObservationV0(body, { clientId });\n if (!result.ok) {\n const status =\n result.error === \"rate_limited\" || result.error === \"agent_spoke_throttled\"\n ? 429\n : result.error === \"payload_too_large\"\n ? 413\n : 400;\n sendJson(res, status, result);\n return;\n }\n sendJson(res, result.idempotent ? 200 : 202, result);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisStream) {\n const corsOrigin = accessControlAllowOriginValue(req);\n if (!corsOrigin && String(process.env.NODE_ENV || \"\").toLowerCase() === \"production\") {\n const og = String(req.headers?.origin || \"\").trim();\n if (og) {\n console.warn(\"[GATEWAY] genesis SSE: missing Access-Control-Allow-Origin for browser Origin (CORS whitelist mismatch):\", og);\n }\n }\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n res.write(\"retry: 3000\\n\\n\");\n res.write(\": genesis continuity stream\\n\\n\");\n res.write(\"event: genesis\\n\");\n res.write(`data: {\"booted\":true,\"pid\":${process.pid}}\\n\\n`);\n registerGenesisSseClient(res, req);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLatest) {\n const cp = getLatestGenesisSignedCheckpoint();\n if (!cp) {\n sendJson(res, 404, { ok: false, error: \"no_checkpoint_yet\", hint: \"wait_for_seq_interval_or_configure_signing_secret\" });\n return;\n }\n sendJson(res, 200, { ok: true, checkpoint: cp });\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(rhizohRuntime.routes.genesisCheckpointBySeqPrefix)) {\n const tail = pathname.slice(rhizohRuntime.routes.genesisCheckpointBySeqPrefix.length).split(\"/\")[0];\n const seq = parseInt(String(tail || \"\").trim(), 10);\n const out = await genesisCheckpointQueryBySeqV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"checkpoint_not_found\") {\n sendJson(res, 404, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointRange) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryRangeV0(from, to);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"range_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLineage) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const seq = parseInt(String(u.searchParams.get(\"seq\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryLineageV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"lineage_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisContinuityEvents) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const limit = parseInt(String(u.searchParams.get(\"limit\") || \"\").trim(), 10);\n const out = await queryGenesisContinuityEventArchiveV0(from, to, type, limit);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\" || out.error === \"event_archive_disabled\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"archive_file_too_large\") {\n sendJson(res, 413, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplay) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await resolveGenesisReplayRouterV1({\n from: fromSeq,\n to: toSeq,\n type,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayDiff) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const out = await computeGenesisReplayTemporalDiffV1({ from: fromSeq, to: toSeq, type });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEquivalence) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from1 = parseInt(String(u.searchParams.get(\"from1\") || \"\").trim(), 10);\n const to1 = parseInt(String(u.searchParams.get(\"to1\") || \"\").trim(), 10);\n const from2 = parseInt(String(u.searchParams.get(\"from2\") || \"\").trim(), 10);\n const to2 = parseInt(String(u.searchParams.get(\"to2\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await compareGenesisReplayEquivalenceV1({\n from1,\n to1,\n from2,\n to2,\n type,\n includeCheckpoints\n });\n if (\n !out.ok &&\n (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")\n ) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayAnalytics) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayAnalyticsV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEvolution) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const collapseWindows = parseInt(String(u.searchParams.get(\"collapseWindows\") || \"0\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayEvolutionV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n collapseWindows,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/metrics\") {\n res.writeHead(200, { \"Content-Type\": \"text/plain; version=0.0.4; charset=utf-8\" });\n res.end(renderInfraMetricsProm());\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.infraPrometheusMetrics) {\n if (process.env.CASTLE_PROMETHEUS_METRICS !== \"1\") {\n sendJson(res, 404, { ok: false, error: \"prometheus_metrics_disabled\" });\n return;\n }\n res.writeHead(200, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(renderRhizohPrometheusMetrics());\n return;\n }\n if (req.method === \"GET\" && pathname.startsWith(\"/infra/traces/\")) {\n const traceId = decodeURIComponent(pathname.slice(\"/infra/traces/\".length));\n const item = await getTraceById(traceId);\n if (!item) return sendJson(res, 404, { ok: false, error: \"trace_not_found\" });\n return sendJson(res, 200, { ok: true, trace: item });\n }\n if (req.method === \"GET\" && pathname === \"/infra/traces\") {\n const u = new URL(String(req.url || \"/infra/traces\"), `http://${req.headers.host || \"localhost\"}`);\n const sessionId = String(u.searchParams.get(\"sessionId\") || \"\").trim();\n const items = sessionId ? await listTracesBySession(sessionId) : [];\n return sendJson(res, 200, { ok: true, sessionId, traces: items });\n }\n\n /** Presence room mesh (C): JOIN / LEAVE / SNAPSHOT / DELTA / REPLAY (HTTP); SUBSCRIBE = SSE stream. */\n if (req.method === \"POST\" && pathname === \"/presence/mesh/join\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid, clientUid: actor.clientUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/leave\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshLeave(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/snapshot\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n sendJson(res, 200, meshSnapshot(roomUid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/delta\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, MAX_MESSAGE_BYTES);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const seq = meshAppendDelta(roomUid, actor.clientUid, {\n node: body?.node,\n projectionPatch: body?.projectionPatch,\n writerSubject: body?.writerSubject\n });\n sendJson(res, 200, { ok: true, roomUid, seq });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/replay\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const fromNodeId = body?.fromNodeId != null ? String(body.fromNodeId) : \"\";\n const fromSeq = body?.fromSeq;\n if (fromNodeId) {\n sendJson(res, 200, meshReplayFromNodeId(roomUid, fromNodeId));\n return;\n }\n sendJson(res, 200, meshReplayFromSeq(roomUid, fromSeq));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/presence/mesh/subscribe\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const roomUid = String(q.searchParams.get(\"roomUid\") || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n const write = (chunk) => {\n try {\n res.write(chunk);\n } catch {\n /* closed */\n }\n };\n const unsub = meshSubscribeSse(roomUid, write);\n const onClose = () => {\n unsub();\n meshLeave(roomUid, actor.clientUid);\n try {\n res.end();\n } catch {\n /* ignore */\n }\n };\n req.on(\"close\", onClose);\n req.on(\"aborted\", onClose);\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, uid: auth.uid, mode: auth.mode, items: await listConnections(auth.uid) });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const id = await createConnection(auth.uid, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"create_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n await updateConnection(auth.uid, id, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"update_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"DELETE\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n await deleteConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"delete_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url?.startsWith(\"/rhizoh/network/presence\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const u = new URL(req.url, \"http://localhost\");\n const roomKey = String(u.searchParams.get(\"room\") || \"world_space_c2c_v0\").slice(0, 64);\n sendJson(res, 200, {\n ok: true,\n roomKey,\n presence: listCastleNetworkPresenceV0(roomKey)\n });\n } catch (error) {\n sendJson(res, 500, { ok: false, error: error?.message || \"presence_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/rhizoh/sync/vault\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const snapshot = await getCloudSyncSnapshotV0(auth.uid);\n sendJson(res, 200, { ok: true, uid: auth.uid, snapshot });\n } catch (error) {\n sendJson(res, 500, { ok: false, error: error?.message || \"cloud_sync_pull_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/rhizoh/sync/vault\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const snapshot = await mergeCloudSyncSnapshotV0(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, uid: auth.uid, snapshot });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"cloud_sync_push_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections/test\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n if (!checkHttpRateLimit(`llm_conn_test:${auth.uid}`, RL_LLM_CONN_TEST_PER_MIN, 60_000)) {\n return sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n }\n const hasBodyKey = !!String(payload?.apiKey || \"\").trim();\n const result = await queryRhizohLlm(\n {\n message: String(payload?.message || \"Ping from Castle Gateway\"),\n context: payload?.context || { source: \"connection_test\" },\n provider: payload?.provider,\n model: payload?.model,\n apiKey: payload?.apiKey || \"\"\n },\n { allowExternalApiKey: true, llmKeySource: hasBodyKey ? \"auto\" : \"env\" }\n );\n logLlmAccess({\n route: \"/llm/connections/test\",\n uid: auth.uid,\n llmKeyBillingOwner: result.llmKeyBillingOwner,\n llmKeyOrigin: result.llmKeyOrigin,\n provider: result.provider,\n model: result.model,\n connectionId: null\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"connection_test_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAgentIdentities(auth.uid);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerAgentIdentity({\n ownerUid: auth.uid,\n agentId: payload?.agentId,\n role: payload?.role,\n personaSeed: payload?.personaSeed || {},\n capabilityLevel: payload?.capabilityLevel\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/agents/identities/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const owns = await getAgentIdentity(id);\n if (!owns || owns.ownerUid !== auth.uid) return sendJson(res, 403, { ok: false, error: \"forbidden\" });\n const payload = await readHttpJson(req);\n const row = await updateAgentIdentity(id, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/academy/events\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/academy/events\"}`);\n const status = String(url.searchParams.get(\"status\") || \"\");\n const items = await listAcademyEvents({ ownerUid: auth.uid, status, limit: 120 });\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/academy/events\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const ev = await queueAcademyEvent({\n type: payload?.type,\n roomId: payload?.roomId,\n ownerUid: auth.uid,\n participants: payload?.participants || [],\n topic: payload?.topic || \"academy\",\n payload: payload?.payload || {}\n });\n sendJson(res, 200, { ok: true, event: ev });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && String(req.url || \"\").startsWith(\"/academy/events/\") && String(req.url || \"\").endsWith(\"/resolve\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const parts = String(req.url || \"\").split(\"/\").filter(Boolean);\n const eventId = parts[2] || \"\";\n try {\n const payload = await readHttpJson(req);\n const out = await resolveAcademyEvent(eventId, payload || {});\n sendJson(res, 200, { ok: true, event: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_resolve_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/memory\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/memory\"}`);\n const scope = url.searchParams.get(\"scope\") === \"agents\" ? \"agents\" : \"users\";\n const id = String(url.searchParams.get(\"id\") || (scope === \"agents\" ? \"\" : auth.uid));\n const kind = String(url.searchParams.get(\"kind\") || \"\");\n const limit = Number(url.searchParams.get(\"limit\") || 40);\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n sendJson(res, 200, { ok: true, scope, id, kind, items: await listMemories({ scope, id, kind, limit }) });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const profile = await getPersonaGoalMemory(auth.uid);\n sendJson(res, 200, { ok: true, profile });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const profile = await setPersonaGoalMemory({\n uid: auth.uid,\n personaPatch: payload?.personaPatch || {},\n goals: Array.isArray(payload?.goals) ? payload.goals : null,\n preferencesPatch: payload?.preferencesPatch || {}\n });\n sendJson(res, 200, { ok: true, profile });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"profile_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/compact\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const out = await autoCompactMemories({ scope, id });\n sendJson(res, 200, { ok: true, scope, id, ...out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_compact_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/context\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const context = await getMemoryContext({\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n query: String(payload?.query || \"\"),\n limit: Number(payload?.limit || 80)\n });\n sendJson(res, 200, { ok: true, context });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_context_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/place-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const placeName = String(payload?.placeName || \"\").trim();\n if (!placeName) return sendJson(res, 400, { ok: false, error: \"placeName_required\" });\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const persona = String(payload?.waypointPersona || payload?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(placeName)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Place: ${placeName}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Give a concise conversational briefing about this place for a walking-route companion.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-place-brief\", placeName, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `PLACE_BRIEF:${placeName} => ${String(summary.reply || \"\").slice(0, 900)}`,\n tags: [\"place\", \"brief\", \"event-layer\"],\n importance: 0.62,\n meta: { placeName }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"place-brief\",\n text: `${placeName} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: {\n placeName,\n directive: summary.directive,\n lat: hit?.lat ? Number(hit.lat) : null,\n lon: hit?.lon ? Number(hit.lon) : null\n }\n });\n sendJson(res, 200, { ok: true, place: hit || null, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"place_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/route-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const waypoints = Array.isArray(payload?.waypoints) ? payload.waypoints.slice(0, 24) : [];\n const out = [];\n for (const wp of waypoints) {\n const name = String(wp?.name || \"\").trim();\n if (!name) continue;\n const persona = String(wp?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(name)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Waypoint: ${name}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Generate a concise spoken briefing for route waypoint.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-route-brief\", waypoint: name, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n out.push({\n name,\n persona,\n place: hit || null,\n reply: summary.reply,\n directive: summary.directive\n });\n }\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `ROUTE_BRIEF: waypoints=${out.length} ${out.map((x) => `${x.name}:${x.persona}`).join(\" | \")}`.slice(0, 1300),\n tags: [\"route\", \"waypoint\", \"brief\", \"event-layer\"],\n importance: 0.74,\n meta: { count: out.length }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"route-brief\",\n text: out.map((x) => `${x.name}:${String(x.reply || \"\").slice(0, 140)}`).join(\" | \").slice(0, 1400),\n roomId: \"studio-main\",\n meta: {\n waypointCount: out.length,\n waypoints: out.map((x) => ({\n name: x.name,\n persona: x.persona,\n lat: x.place?.lat ? Number(x.place.lat) : null,\n lon: x.place?.lon ? Number(x.place.lon) : null\n }))\n }\n });\n sendJson(res, 200, { ok: true, items: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"route_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/pdf-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const title = String(payload?.title || \"document\");\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const providedText = String(payload?.text || \"\");\n const url = String(payload?.url || \"\");\n let content = providedText;\n if (!content && url) content = await fetchTextFromUrl(url);\n if (!content) return sendJson(res, 400, { ok: false, error: \"text_or_supported_url_required\" });\n const summary = await queryRhizohLlm(\n {\n message: `Summarize this document and provide key points + practical actions.\\nTitle:${title}\\nContent:\\n${content.slice(0, 12000)}`,\n context: { source: \"event-layer-pdf-brief\", title },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `DOC_BRIEF:${title} => ${String(summary.reply || \"\").slice(0, 1000)}`,\n tags: [\"document\", \"brief\", \"library\", \"event-layer\"],\n importance: 0.7,\n meta: { title, url: url || null }\n });\n await addTranscript(auth.uid, {\n source: \"library-brief\",\n eventType: \"pdf-brief\",\n text: `${title} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { title }\n });\n sendJson(res, 200, { ok: true, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/pdf/upload\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req, 8 * 1024 * 1024);\n const job = await enqueuePdfJob({\n uid: auth.uid,\n fileName: payload?.fileName || \"document.pdf\",\n bytesBase64: payload?.contentBase64\n });\n sendJson(res, 200, { ok: true, job });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_upload_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/session\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createPublishSession(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session\",\n text: `Publish session armed: ${row.protocol} ${row.target}`.slice(0, 500),\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: row.id, bridge: row.bridge }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/studio/publish/sessions\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listPublishSessions(auth.uid, 80);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/health-check\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const protocol = String(payload?.protocol || \"WHIP\");\n const target = String(payload?.target || \"\");\n const health = await checkPublishTargetHealth(protocol, target);\n if (payload?.sessionId) {\n await updatePublishSession(auth.uid, String(payload.sessionId), {\n status: health.ok ? \"healthy\" : \"degraded\",\n reconnectPolicy: {\n enabled: true,\n backoffSec: [2, 5, 10, 20],\n nextRetryInSec: health.reconnectInSec || 0,\n maxRetries: 8,\n reason: health.reason\n }\n });\n }\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-health\",\n text: `${protocol} ${target} => ${health.ok ? \"healthy\" : \"degraded\"} (${health.reason})`,\n roomId: \"studio-main\",\n meta: { protocol, target, reconnect: !!health.shouldReconnect }\n });\n sendJson(res, 200, { ok: true, health });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_health_check_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/studio/publish/sessions/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updatePublishSession(auth.uid, id, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session-update\",\n text: `Session ${id} -> ${row.status || \"updated\"}`,\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: id, status: row.status, ingestUrl: row.ingestUrl || \"\" }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/transcripts\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/transcripts\"}`);\n const limit = Math.max(1, Math.min(200, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await listTranscripts(auth.uid, limit);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/transcripts\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await addTranscript(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"transcript_write_failed\" });\n }\n return;\n }\n\n /** E1: broadcast intent → GreenRoom capability → transcript + memory artifact */\n if (req.method === \"POST\" && req.url === \"/studio/capabilities/greenroom\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const traceId = String(payload?.traceId || `TRC-${Date.now().toString(36).toUpperCase()}`);\n const title = String(payload?.title || \"Castle Live Broadcast\").slice(0, 200);\n const intentRaw = String(payload?.intentRaw || \"\").slice(0, 800);\n const audienceEstimate = Math.max(0, Math.min(50_000_000, Number(payload?.audienceEstimate) || 1200));\n const roomId = String(payload?.roomId || \"greenroom-main\");\n const transcriptRow = await addTranscript(auth.uid, {\n source: \"rhizoh\",\n eventType: \"BROADCAST_ROUTED\",\n text: `GreenRoom · ${title}${intentRaw ? `\\nIntent: ${intentRaw}` : \"\"}`,\n roomId,\n meta: {\n ack: \"BROADCAST_ROUTED\",\n capability: \"greenroom\",\n intentType: \"broadcast\",\n traceId,\n audienceEstimate,\n artifact: {\n kind: \"broadcast_card\",\n title,\n traceId,\n roomId,\n createdAt: Date.now()\n }\n }\n });\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n text: `GREENROOM_BROADCAST:${title} trace=${traceId}`,\n tags: [\"broadcast\", \"greenroom\", \"artifact\", \"castle-library\"],\n importance: 0.72,\n kind: \"episodic\",\n meta: { traceId, capability: \"greenroom\", transcriptId: transcriptRow.id }\n });\n sendJson(res, 200, {\n ok: true,\n ack: \"BROADCAST_ROUTED\",\n traceId,\n transcript: transcriptRow,\n replayPath: `/replay/${encodeURIComponent(traceId)}`,\n sharePath: `/replay/${encodeURIComponent(traceId)}`\n });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"greenroom_capability_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/metadata/search\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/metadata/search\"}`);\n const q = String(url.searchParams.get(\"q\") || \"\");\n const limit = Math.max(1, Math.min(300, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await searchMetadata(auth.uid, q, limit);\n sendJson(res, 200, { ok: true, q, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerDevice(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, device: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"device_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const lat = Number(payload?.lat);\n const lon = Number(payload?.lon);\n const speed = Math.max(0, Number(payload?.speed || 0));\n const cfgCenterLat = Number(process.env.CASTLE_GEOFENCE_CENTER_LAT || 41.0082);\n const cfgCenterLon = Number(process.env.CASTLE_GEOFENCE_CENTER_LON || 28.9784);\n const cfgRadiusM = Math.max(100, Number(process.env.CASTLE_GEOFENCE_RADIUS_M || 55000));\n const cfgSpeedCap = Math.max(1, Number(process.env.CASTLE_SPEED_CAP_MPS || 35));\n const dist = Number.isFinite(lat) && Number.isFinite(lon) ? haversineMeters(cfgCenterLat, cfgCenterLon, lat, lon) : 0;\n const geofenceOk = !Number.isFinite(dist) ? true : dist <= cfgRadiusM;\n const speedOk = speed <= cfgSpeedCap;\n const failsafe = !(geofenceOk && speedOk);\n const safety = {\n geofenceOk,\n speedOk,\n failsafe,\n distanceFromCenterM: Number.isFinite(dist) ? dist : null,\n speedCapMps: cfgSpeedCap\n };\n const row = await appendTelemetry(auth.uid, deviceId, {\n lat,\n lon,\n speed,\n battery: payload?.battery,\n mode: payload?.mode || (failsafe ? \"FAILSAFE_HOLD\" : \"ACTIVE\"),\n raw: payload?.raw || {}\n });\n await updateDevice(auth.uid, deviceId, {\n status: failsafe ? \"failsafe\" : \"active\",\n lastTelemetryAt: row.ts,\n safetyState: safety\n });\n if (failsafe) {\n await addTranscript(auth.uid, {\n source: \"robotics-guardrails\",\n eventType: \"failsafe\",\n text: `Device ${deviceId} failsafe triggered geofenceOk=${geofenceOk} speedOk=${speedOk}`,\n roomId: \"studio-main\",\n meta: { deviceId, safety }\n });\n }\n sendJson(res, 200, { ok: true, telemetry: row, safety, command: failsafe ? \"FAILSAFE_HOLD\" : \"CONTINUE\" });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"telemetry_ingest_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listTelemetry(auth.uid, 150);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createAutomation(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"automation-scheduled\",\n text: `Automation scheduled: ${row.title} at ${new Date(row.scheduleAt).toLocaleString()}`,\n roomId: \"studio-main\",\n meta: { automationId: row.id, action: row.action, deviceId: row.deviceId }\n });\n sendJson(res, 200, { ok: true, automation: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"automation_create_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAutomations(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listDevices(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const devs = await listDevices(auth.uid, 500);\n const d = devs.find((x) => x.id === deviceId);\n if (!d) return sendJson(res, 404, { ok: false, error: \"device_not_found\" });\n const action = String(payload?.action || \"\");\n const caps = Array.isArray(d.capabilityProfile) ? d.capabilityProfile : [];\n if (action && caps.length && !caps.includes(action) && !caps.includes(\"*\")) {\n return sendJson(res, 400, { ok: false, error: \"unsupported_action_for_device\", capabilityProfile: caps });\n }\n const cmd = await enqueueCommand(auth.uid, {\n deviceId,\n action: action || \"NOOP\",\n params: payload?.params || {},\n adapter: payload?.adapter || d.adapter || \"websocket\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-command-queue\",\n eventType: \"command-queued\",\n text: `${cmd.deviceId} -> ${cmd.action}`,\n roomId: \"studio-main\",\n meta: { commandId: cmd.id, adapter: cmd.adapter }\n });\n sendJson(res, 200, { ok: true, command: cmd });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listCommands(auth.uid, 200);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/robotics/commands/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updateCommandStatus(auth.uid, id, payload?.status || \"sent\");\n sendJson(res, 200, { ok: true, command: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/rhizoh/bridge\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const result = await runRhizohBrainV2({\n message: String(payload?.message || \"robotics bridge command\"),\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n worldState: payload?.worldState || {},\n deviceState: payload?.deviceState || {},\n intent: \"robotics-mechanics\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-bridge\",\n eventType: \"robotics-command\",\n text: `Bridge -> ${String(result.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { directive: result.directive, agentId: payload?.agentId || \"\" }\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"robotics_bridge_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/planner/graph\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const graph = buildTaskActionGraph(String(payload?.task || \"\"), payload?.context || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"planner-graph\",\n text: `Planner graph built: ${graph.taskTitle} nodes=${graph.nodes.length}`,\n roomId: \"studio-main\",\n meta: { taskTitle: graph.taskTitle, nodeCount: graph.nodes.length }\n });\n sendJson(res, 200, { ok: true, graph });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"planner_graph_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerSocialChannel(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, channel: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_channel_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listSocialChannels(auth.uid, 100);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/broadcast\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const text = String(payload?.text || \"\").slice(0, 1200);\n const channels = await listSocialChannels(auth.uid, 100);\n const enabled = channels.filter((c) => c.status === \"enabled\");\n const deliveries = [];\n for (const c of enabled) {\n const out = await deliverSocialWithRetry(c, text);\n deliveries.push({ id: c.id, platform: c.platform, ...out });\n }\n const okCount = deliveries.filter((d) => d.ok).length;\n await addTranscript(auth.uid, {\n source: \"social-layer\",\n eventType: \"social-broadcast\",\n text: `Broadcast delivered ${okCount}/${enabled.length} channel(s): ${text.slice(0, 240)}`,\n roomId: \"studio-main\",\n meta: { deliveries }\n });\n sendJson(res, 200, { ok: true, sentChannels: okCount, totalChannels: enabled.length, deliveries });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_broadcast_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/ethics/programs\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, ...buildEthicsPrograms() });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands/dispatch\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const all = await listCommands(auth.uid, 200);\n const queued = all.filter((c) => String(c.status || \"queued\") === \"queued\").slice(0, 50);\n const devices = await listDevices(auth.uid, 500);\n const byId = new Map(devices.map((d) => [d.id, d]));\n const results = [];\n for (const cmd of queued) {\n const dev = byId.get(cmd.deviceId);\n if (!dev) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: \"device_not_found\" });\n continue;\n }\n try {\n const dispatched = await dispatchCommandToAdapter(dev, cmd);\n await updateCommandStatus(auth.uid, cmd.id, \"sent\");\n results.push({ id: cmd.id, ok: true, adapter: dispatched.adapter, via: dispatched.via });\n } catch (error) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: String(error?.message || \"dispatch_failed\") });\n }\n }\n await addTranscript(auth.uid, {\n source: \"robotics-command-dispatch\",\n eventType: \"command-dispatch-batch\",\n text: `Dispatch batch size=${results.length} ok=${results.filter((x) => x.ok).length}`,\n roomId: \"studio-main\",\n meta: { results: results.slice(0, 30) }\n });\n sendJson(res, 200, { ok: true, processed: results.length, results });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"dispatch_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/pdf/jobs/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n const job = await getPdfJob(auth.uid, id);\n if (!job) return sendJson(res, 404, { ok: false, error: \"pdf_job_not_found\" });\n sendJson(res, 200, { ok: true, job });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const row = await appendMemory({\n scope,\n id,\n text: payload?.text,\n tags: payload?.tags || [],\n importance: payload?.importance,\n kind: payload?.kind || \"episodic\",\n meta: { uid: auth.uid, ...(payload?.meta || {}) }\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.externalTruth) {\n try {\n if (process.env.RHIZOH_EXTERNAL_TRUTH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_truth:${ip}`, RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n sendJson(res, 200, buildRhizohExternalGroundTruthPayload(req));\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.productOutcome) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const rawBody = await readHttpBodyText(req, 64 * 1024);\n const body = rawBody.trim() ? safeJsonParse(rawBody) : null;\n if (!body || typeof body !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"invalid_json_payload\" });\n return;\n }\n const sigHdr = String(req.headers?.[\"x-rhizoh-outcome-signature\"] || \"\").trim();\n const hmacV = verifyRhizohOutcomeHmac(rawBody, sigHdr);\n if (!hmacV.ok) {\n sendJson(res, 401, { ok: false, error: hmacV.error || \"outcome_hmac_invalid\" });\n return;\n }\n const srcTokHdr = String(req.headers?.[\"x-rhizoh-outcome-source-token\"] || \"\").trim();\n const srcV = verifyRhizohOutcomeSourceToken(body.source, srcTokHdr);\n if (!srcV.ok) {\n sendJson(res, 401, { ok: false, error: srcV.error || \"outcome_source_token_invalid\" });\n return;\n }\n const subjectKey = String(body.subjectKey || \"\").trim().slice(0, 128);\n if (\n subjectKey &&\n !checkHttpRateLimit(\n `rhizoh_product_outcome_subj:${subjectKey}`,\n RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN,\n 60_000\n )\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_subject\" });\n return;\n }\n const result = ingestRhizohProductOutcomeHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n schemaVersion: \"1.1.0\",\n aggregate: result.aggregate,\n integrity: result.integrity\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"outcome_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.productOutcomeAggregate) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome_agg:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const u = new URL(String(req.url || \"/\"), \"http://localhost\");\n const cohortId = u.searchParams.get(\"cohortId\") || \"\";\n const decisionFingerprint = u.searchParams.get(\"decisionFingerprint\") || \"\";\n const limit = u.searchParams.get(\"limit\") || \"50\";\n const rows = listRhizohProductOutcomeAggregates({\n cohortId: cohortId || undefined,\n decisionFingerprint: decisionFingerprint || undefined,\n limit: Number(limit) || 50\n });\n sendJson(res, 200, { ok: true, schemaVersion: \"1.1.0\", rows });\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.externalLossBatch) {\n try {\n if (process.env.RHIZOH_EXTERNAL_LOSS_BATCH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_loss_batch:${ip}`, RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const result = ingestRhizohExternalLossBatchHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, { ok: true, accepted: result.accepted ?? 0 });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"batch_failed\") });\n }\n return;\n }\n\n function moderationAdminOk(req) {\n const expected = String(process.env.CASTLE_MODERATION_ADMIN_KEY || \"\").trim();\n if (!expected) return false;\n const got = String(req.headers[\"x-castle-moderation-key\"] || \"\").trim();\n return got.length > 0 && got === expected;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.opsAbuseReport) {\n try {\n const body = await readHttpJson(req, 32 * 1024);\n const auth = await resolveHttpUser(req);\n const entry = submitAbuseReportV0({\n reporterUid: auth.ok ? auth.uid : null,\n targetUid: body?.targetUid ?? null,\n category: body?.category,\n detail: body?.detail,\n traceId: body?.traceId ?? null\n });\n sendJson(res, 202, { ok: true, reportId: entry.id, status: entry.status });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsModerationQueue) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n queue: listModerationQueueV0(100),\n softBlocked: listSoftBlockedUsersV0()\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-block\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const out = softBlockUserV0(body?.uid, body?.reason);\n sendJson(res, out.ok ? 200 : 400, out);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-unblock\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n sendJson(res, 200, removeSoftBlockV0(body?.uid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsAgentSnapshots) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n const limit = Number(new URL(req.url || \"\", \"http://local\").searchParams.get(\"limit\") || 50);\n sendJson(res, 200, { ok: true, snapshots: listRecentAgentSnapshotsV0(limit) });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.academicObservatoryExport) {\n const auth = await resolveHttpUser(req);\n const out = handleAcademicObservatoryExportGetV0(req, auth);\n sendJson(res, out.status, out.body);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsHardeningStatus) {\n const auth = await resolveHttpUser(req);\n const principal = auth.ok ? `uid:${auth.uid}` : null;\n const gclSnap = await getGlobalCostLedgerSnapshotV0(principal);\n const economicStrategy = evaluateEconomicStrategyL3V0(gclSnap);\n const hardeningTenantId = resolveHardeningTenantIdV0(auth, req);\n const unifiedState = await buildUnifiedStateNarrativeV0({\n tenantId: hardeningTenantId ?? undefined,\n platformScope: !hardeningTenantId,\n principal\n });\n sendJson(res, 200, {\n ok: true,\n schema: \"rhizoh.ops.hardening_status.v0\",\n containment: readAgentContainmentConfigV0(),\n cost: readCostContainmentConfigV0(),\n gcl: {\n backend: await getGclBackendStatusV0(),\n snapshot: gclSnap\n },\n economicStrategyL3: economicStrategy,\n phasedRollout: await getPhasedRolloutStatsV0(),\n tier: readPhasedRolloutTierV0(),\n principalCost: auth.ok ? await getCostContainmentStatsV0(principal) : null,\n stressTaxonomy: verifyStressTaxonomyCoverageV0(),\n stressConflictResolution: verifyStressConflictResolutionV0(),\n resolutionStabilityEnvelope: verifyResolutionStabilityEnvelopeV0(),\n unifiedState: {\n tenantScope: {\n tenantId: unifiedState.tenantScope?.tenantId,\n scope: unifiedState.tenantScope?.scope,\n noGlobalDerivedState: unifiedState.tenantScope?.noGlobalDerivedState\n },\n narrativeFingerprint: unifiedState.narrativeFingerprint,\n screenshotScopeWatermark: unifiedState.screenshotScopeWatermark,\n interpretationSafetyContract: unifiedState.interpretationSafetyContract,\n interpretationUxContract: {\n schema: unifiedState.interpretationUxContract?.schema,\n layerOrder: unifiedState.interpretationUxContract?.layerOrder,\n narrativeNeverVisuallyDominant: unifiedState.interpretationUxContract?.narrativeNeverVisuallyDominant\n },\n governance: {\n narrativeIsNotDecisionLayer: unifiedState.governance.narrativeIsNotDecisionLayer,\n displayOrder: unifiedState.governance.displayOrder,\n trustPosture: unifiedState.governance.trustPosture,\n interpretationUxContract: unifiedState.governance.interpretationUxContract?.schema\n },\n humanOps: {\n decisionTier: unifiedState.humanOps?.humanDecisionScaling?.tier,\n decisionOwner: unifiedState.humanOps?.humanDecisionScaling?.decisionOwner,\n opsRouteId: unifiedState.humanOps?.humanDecisionOpsRunbook?.routing?.routeId,\n slaAckMinutes: unifiedState.humanOps?.humanDecisionOpsRunbook?.sla?.ackMinutes,\n uiDisclaimerRequired: unifiedState.humanOps?.narrativeUiSafety?.displayRules?.requireDisclaimerBanner,\n misreadHighResidual: unifiedState.humanOps?.misreadSimulation?.highResidualCount,\n propagationHighResidual: unifiedState.humanOps?.socialPropagationSimulation?.highResidualCount,\n worstWatermarkSurvivability:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.worstWatermarkSurvivability,\n dominantDistortionSource:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.dominantDistortionSource\n },\n culturalRisk: {\n perceptualRiskClass: unifiedState.culturalRisk?.perceptualRiskClass,\n trustFork: unifiedState.culturalRisk?.trustDynamics?.fork,\n trustDecayScore: unifiedState.culturalRisk?.trustDynamics?.scores?.trustDecay,\n mythologyScore: unifiedState.culturalRisk?.trustDynamics?.scores?.mythology,\n confidenceDetachmentExposure:\n unifiedState.culturalRisk?.trustDynamics?.axes?.confidenceDetachmentExposure\n },\n appliedSystemsLayer: {\n schema: unifiedState.appliedSystemsLayer?.schema,\n researchOutputType: unifiedState.appliedSystemsLayer?.researchGovernance?.outputType,\n roboticsBlockActuation: unifiedState.appliedSystemsLayer?.roboticsGrounding?.blockActuation,\n spiralRumorCount:\n unifiedState.appliedSystemsLayer?.spiralMMOGameKernel?.gameLayer?.rumors?.length\n },\n actionSemanticGovernance: {\n schema: unifiedState.actionSemanticGovernance?.schema,\n highRiskActionCount: unifiedState.actionSemanticGovernance?.highRiskActionCount,\n loopClosed: unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.loopClosed,\n authorityCollapseMitigated:\n unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.risks?.authorityCollapse\n ?.mitigated\n },\n actionContextResolution: {\n schema: unifiedState.actionContextResolution?.schema,\n contextFingerprint: unifiedState.actionContextResolution?.contextFingerprint?.fingerprintHash,\n domainCollision: unifiedState.actionContextResolution?.domainCollision?.detected,\n deployAgentSpiralBlocked:\n unifiedState.actionContextResolution?.actionOverloadingRisk?.deployAgentExample\n ?.spiral_mmo === \"blocked\",\n roboticsActuationEligible:\n unifiedState.actionContextResolution?.roboticsActuationCorrectness?.actuationEligible\n },\n epistemicCoherence: {\n schema: unifiedState.epistemicCoherence?.schema,\n verdict: unifiedState.epistemicCoherence?.systemCoherence?.verdict,\n coherenceScore: unifiedState.epistemicCoherence?.systemCoherence?.score,\n contradictionCount: unifiedState.epistemicCoherence?.crossLayerContradictions?.count,\n operatorDecision: unifiedState.epistemicCoherence?.uxCompression?.operatorDecision,\n headlineTr: unifiedState.epistemicCoherence?.uxCompression?.headline?.tr,\n falseSafetyWarning: Boolean(\n unifiedState.epistemicCoherence?.uxCompression?.falseSafetyWarning\n )\n },\n coherenceAuthorityBoundary: {\n schema: unifiedState.coherenceAuthorityBoundary?.schema,\n bindingValid: unifiedState.coherenceAuthorityBoundary?.eclAuthorityBinding?.valid,\n canExecute: unifiedState.coherenceAuthorityBoundary?.contract?.can_execute,\n mandatoryBannerTr:\n unifiedState.coherenceAuthorityBoundary?.disclaimers?.mandatoryBanner?.tr\n },\n decisionLatencyGovernance: {\n schema: unifiedState.decisionLatencyGovernance?.schema,\n latencyTier: unifiedState.decisionLatencyGovernance?.latencyTier,\n inflationScore: unifiedState.decisionLatencyGovernance?.latencyInflation?.score,\n fastPathEligible: unifiedState.decisionLatencyGovernance?.fastPath?.eligible,\n primaryActionId: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.primaryActionId,\n ackSlaMinutes: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.ackSlaMinutes,\n certaintyCap: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.certaintyCap,\n uncertaintyItems:\n unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.uncertaintyEnvelope?.itemCount\n },\n decisionPacketUncertaintyBoundary: {\n schema: unifiedState.decisionPacketUncertaintyBoundary?.schema,\n invariantValid: unifiedState.decisionPacketUncertaintyBoundary?.invariantCheck?.valid,\n centralInvariant: unifiedState.decisionPacketUncertaintyBoundary?.centralInvariant\n },\n epistemicDecisionPacing: {\n schema: unifiedState.epistemicDecisionPacing?.schema,\n queueSaturationTier:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation?.tier,\n operatorProcessingLatency:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation\n ?.operatorProcessingLatency,\n postureWindowCount:\n unifiedState.epistemicDecisionPacing?.temporaryStaticPostureWindows\n ?.temporaryStaticPostureWindows?.windowCount,\n executionBoundariesValid:\n unifiedState.epistemicDecisionPacing?.executionBoundaries?.valid,\n grantsExecutionApproval: unifiedState.epistemicDecisionPacing?.grantsExecutionApproval\n },\n epistemicTemporalCoherence: {\n schema: unifiedState.epistemicTemporalCoherence?.schema,\n allInvariantsValid: unifiedState.epistemicTemporalCoherence?.allInvariantsValid,\n temporalAlignmentValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.temporalAlignment?.valid,\n crossWindowGuardValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard?.valid,\n delayedTruthValid: unifiedState.epistemicTemporalCoherence?.invariants?.delayedTruthRule?.valid,\n reconciliationRequired:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard\n ?.reconciliation?.required\n },\n realityDriftObserver: {\n schema: unifiedState.realityDriftObserver?.schema,\n driftActive: unifiedState.realityDriftObserver?.driftObserverSummary?.active,\n misapprehensionShapeCount:\n unifiedState.realityDriftObserver?.misapprehensionShapeCatalog?.shapeCount,\n criticalShapeCount:\n unifiedState.realityDriftObserver?.driftObserverSummary?.criticalShapeCount,\n roboticsMismatchActive:\n unifiedState.realityDriftObserver?.roboticsFeedbackMismatch?.active\n },\n driftCausality: {\n schema: unifiedState.driftCausality?.schema,\n shapesWithCausalExplanation: unifiedState.driftCausality?.rdolLinkage?.shapesWithCausalExplanation,\n activeDomainCount: unifiedState.driftCausality?.causalitySummary?.activeDomainCount,\n propagationCausalityActive: unifiedState.driftCausality?.causalDomains?.propagation?.active\n },\n productReadiness: unifiedState.productReadiness,\n stateLayers: {\n raw: unifiedState.stateLayers.raw,\n derived: {\n systemState: unifiedState.stateLayers.derived.systemState,\n narrative: unifiedState.stateLayers.derived.narrative,\n validation: unifiedState.stateLayers.derived.validation\n },\n policy: {\n binding: unifiedState.stateLayers.policy.binding,\n decisionOwner: unifiedState.stateLayers.policy.decisionOwner,\n prohibitedActions: unifiedState.stateLayers.policy.prohibitedActions\n }\n },\n systemState: unifiedState.systemState,\n interpretation: unifiedState.interpretation,\n validation: {\n trustPosture: unifiedState.validation.trustPosture,\n adjustedConfidence: unifiedState.validation.adjustedConfidence,\n agreement: unifiedState.validation.confidenceDecomposition.agreement,\n divergenceScore: unifiedState.validation.divergence.divergenceScore,\n validated: unifiedState.validation.divergence.validated,\n uncertaintyTags: unifiedState.validation.uncertainty.tags.map((t) => t.id),\n flags: unifiedState.validation.divergence.flags.map((f) => f.id)\n },\n compression: unifiedState.compression\n }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 6 * 1024 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (\n !checkHttpRateLimit(`rhizoh_voice_transcribe:${rlKey}`, RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN, 60_000)\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const voiceEnv = rhizohVoiceTranscribeEnvV3();\n if (!voiceEnv.any) {\n sendJson(res, 503, {\n ok: false,\n error: \"voice_asr_not_configured\",\n env: voiceEnv\n });\n return;\n }\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n const result = await runRhizohVoiceTranscribeV3(\n applyLanguagePropagationToVoiceBodyV1(payload, langProp)\n );\n sendJson(res, result.ok ? 200 : 422, {\n ...result,\n traceId: String(payload?.traceId || \"\"),\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n atMs: Date.now()\n });\n } catch (error) {\n sendJson(res, 500, {\n ok: false,\n error: \"voice_transcribe_failed\",\n detail: String(error?.message || error),\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(\"/rhizoh/llm/task/\")) {\n try {\n const rawTaskId = decodeURIComponent(pathname.slice(\"/rhizoh/llm/task/\".length).split(\"/\")[0] || \"\");\n const taskId = String(rawTaskId || \"\").toLowerCase() === \"last\" ? getLatestLlmWorkerTaskIdV0() : rawTaskId;\n const snap = taskId ? getLlmWorkerTaskSnapshotV0(taskId) : null;\n if (!snap) {\n sendJson(res, 404, {\n ok: false,\n error: \"llm_task_not_found\",\n taskId: rawTaskId,\n status: \"missing\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n if (snap.status === \"processing\") {\n sendJson(res, 202, {\n ok: true,\n taskId: snap.taskId,\n status: \"processing\",\n createdAt: snap.createdAt,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n // Terminal states (completed/failed) always HTTP 200 — poll must not look like server fault (500).\n sendJson(res, 200, {\n ...(snap.body || { ok: false, error: \"llm_task_empty\", taskId: snap.taskId }),\n taskId: snap.taskId,\n status: snap.status === \"completed\" ? \"completed\" : snap.status,\n terminal: true,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n } catch (error) {\n console.error(\"[llm-task-poll] handler failed:\", error);\n sendJson(res, 200, {\n ok: false,\n error: \"llm_task_poll_handler_failed\",\n detail: String(error?.message || error),\n status: \"failed\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === rhizohRuntime.routes.rhizohLlm) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 128 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n logRhizohHealth(\"gateway_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: String(payload?.traceId || \"\"),\n auth: auth.ok ? \"ok\" : \"anon\",\n ip\n });\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (!checkHttpRateLimit(`rhizoh_llm:${rlKey}`, RL_RHIZOH_LLM_PER_MIN, 60_000)) {\n const rlTaxonomy = classifyStressResponseV0({ code: \"rate_limit_exceeded\" });\n return sendJson(res, 429, {\n ok: false,\n error: \"rate_limit_exceeded\",\n reply: rlTaxonomy.userMessageTr || \"İstek sınırı aşıldı. Kısa süre sonra tekrar deneyin.\",\n stressClass: rlTaxonomy.stressClass,\n responseAction: rlTaxonomy.responseAction,\n stressMatrix: rlTaxonomy.matrix,\n stressInterpretable: rlTaxonomy.interpretable,\n stressConfidence: rlTaxonomy.stressConfidence,\n actionConfidence: rlTaxonomy.actionConfidence,\n actionSoftened: rlTaxonomy.actionSoftened,\n responseActionStrict: rlTaxonomy.responseActionStrict,\n actionInterpretable: rlTaxonomy.actionInterpretable,\n conflictResolution: rlTaxonomy.conflictResolution,\n directive: \"NONE\"\n });\n }\n\n const requireExplicit = process.env.CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE === \"1\";\n const keyMode = normalizeClientLlmKeySource(payload?.llmKeySource ?? payload?.keySource);\n if (keyMode === null) {\n return sendJson(res, 400, {\n ok: false,\n error: \"invalid_llm_key_source\",\n reply: \"llmKeySource geçersiz. Kullanın: env | user_connection | auto\",\n directive: \"NONE\"\n });\n }\n if (requireExplicit && keyMode === \"auto\") {\n return sendJson(res, 400, {\n ok: false,\n error: \"llm_key_source_required\",\n reply: \"Sunucu CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE=1: istekte llmKeySource (env veya user_connection) zorunlu.\",\n directive: \"NONE\"\n });\n }\n\n const conn = auth.ok ? await resolveConnection(auth.uid, String(payload?.connectionId || \"\")) : null;\n\n if (keyMode === \"user_connection\") {\n if (!auth.ok) {\n return sendJson(res, 401, {\n ok: false,\n error: \"llm_auth_required_for_user_connection\",\n reply: \"Kayıtlı LLM bağlantısı için giriş gerekli.\",\n directive: \"NONE\"\n });\n }\n if (!conn?.apiKey) {\n return sendJson(res, 400, {\n ok: false,\n error: \"user_llm_connection_required\",\n reply: \"Geçerli bir LLM bağlantısı yok; Studio’da bağlantı ekleyin veya llmKeySource=env kullanın.\",\n directive: \"NONE\"\n });\n }\n }\n\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n let safePayload = applyLanguagePropagationToLlmPayloadV1({ ...(payload || {}) }, langProp);\n const trimOut = trimRhizohLlmGatewayPayloadV1(safePayload);\n if (trimOut.meta.trimmed) {\n logRhizohHealth(\"llm_payload_trimmed\", {\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n bytesBefore: trimOut.meta.bytesBefore,\n bytesAfter: trimOut.meta.bytesAfter\n });\n }\n safePayload = trimOut.payload;\n delete safePayload.apiKey;\n delete safePayload.llmKeySource;\n delete safePayload.keySource;\n\n const llmMessage = String(\n safePayload?.message || safePayload?.text || payload?.message || payload?.text || \"\"\n ).trim();\n if (!llmMessage) {\n return sendJson(res, 400, {\n ok: false,\n error: \"message_required\",\n reply: \"Mesaj metni gerekli (message alanı boş).\",\n directive: \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n if (!safePayload.message) {\n safePayload = { ...safePayload, message: llmMessage };\n }\n\n let resolvedProvider = payload?.provider;\n let resolvedModel = payload?.model;\n let connApiKey = \"\";\n\n ({\n resolvedProvider,\n resolvedModel,\n connApiKey\n } = resolveServerLlmProviderV0(keyMode, conn, payload));\n\n const turnInput = {\n safePayload,\n auth,\n keyMode,\n conn,\n resolvedProvider,\n resolvedModel,\n connApiKey\n };\n const turnMeta = {\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n langEcho: buildRhizohLanguagePropagationEchoV1(langProp)\n };\n\n if (\n isLlmWorkerEnabledV0() &&\n isLlmWorkerAsyncEnabledV0() &&\n String(req.headers[\"x-rhizoh-llm-sync\"] || \"\").trim() !== \"1\"\n ) {\n const taskId = enqueueLlmWorkerTaskAsyncV0(turnInput, turnMeta);\n logRhizohHealth(\"llm_worker_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n taskId,\n traceId: String(payload?.traceId || taskId),\n keyMode,\n async: true\n });\n sendJson(res, 202, {\n ok: true,\n taskId,\n status: \"processing\",\n traceId: String(payload?.traceId || taskId),\n pollPath: `/rhizoh/llm/task/${taskId}`,\n gatewayPid: process.pid,\n ...turnMeta.langEcho\n });\n return;\n }\n\n const { result, traceId, turnLatencyMs, spinePhases, sampledTrace } = isLlmWorkerEnabledV0()\n ? await runLlmWorkerTaskV0(turnInput, turnMeta)\n : await rhizohGatewayTurn(turnInput);\n logRhizohHealth(\"llm_response\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId,\n turnLatencyMs: Number(turnLatencyMs || 0),\n keyMode\n });\n sendJson(res, 200, {\n ...result,\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n traceId,\n turnLatencyMs,\n ...(spinePhases ? { spinePhases } : {}),\n ...(sampledTrace !== undefined ? { sampledTrace } : {})\n });\n } catch (error) {\n const msg = String(error?.message || \"\");\n const code = error?.code || \"\";\n let status = 500;\n if (msg === \"payload_too_large\") status = 413;\n else if (msg === \"rate_limit_exceeded\") status = 429;\n else if (\n code === \"cost_hard_limit\" ||\n code === \"phased_rollout_capacity\" ||\n error?.containment === true ||\n String(code).startsWith(\"agent_\")\n ) {\n status = 429;\n } else if (code === \"user_soft_blocked\" || code === \"prompt_abuse_detected\") status = 403;\n else if (\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(code) ||\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(msg) ||\n msg.includes(\"missing_api_key_for_\")\n )\n status = 400;\n let rhizohFailureKind = \"provider_error\";\n if (msg === \"rate_limit_exceeded\" || status === 429) rhizohFailureKind = \"rate_limit\";\n else if (status === 403) rhizohFailureKind = \"policy_block\";\n else if (status === 400 || status === 413) rhizohFailureKind = \"client_config\";\n else if (msg.startsWith(\"provider_http_\")) {\n const st = Number(msg.replace(\"provider_http_\", \"\"));\n if (st === 408 || st === 504) rhizohFailureKind = \"timeout\";\n else rhizohFailureKind = \"provider_error\";\n } else if (/timeout|timed out|ETIMEDOUT/i.test(msg)) rhizohFailureKind = \"timeout\";\n\n const providerHttpMatch = msg.match(/^provider_http_(\\d{3})$/);\n const stressTaxonomy =\n error?.stressClass != null\n ? {\n stressClass: error.stressClass,\n responseAction: error.responseAction,\n stressMatrix: error.stressMatrix,\n stressInterpretable: error.stressInterpretable !== false,\n stressConfidence: error.stressConfidence,\n actionConfidence: error.actionConfidence,\n actionSoftened: error.actionSoftened,\n responseActionStrict: error.responseActionStrict,\n actionInterpretable: error.actionInterpretable,\n conflictResolution: error.conflictResolution,\n stressSecondary: error.stressSecondary,\n responseActions: error.responseActions\n }\n : (() => {\n const t = classifyStressResponseV0({\n code: code || msg,\n rhizohFailureKind,\n providerHttpStatus: providerHttpMatch\n ? Number(providerHttpMatch[1])\n : null\n });\n return {\n stressClass: t.stressClass,\n responseAction: t.responseAction,\n stressMatrix: t.matrix,\n stressInterpretable: t.interpretable,\n stressConfidence: t.stressConfidence,\n actionConfidence: t.actionConfidence,\n actionSoftened: t.actionSoftened,\n responseActionStrict: t.responseActionStrict,\n actionInterpretable: t.actionInterpretable,\n conflictResolution: t.conflictResolution,\n stressSecondary: t.stressSecondary,\n responseActions: t.responseActions\n };\n })();\n sendJson(res, status, {\n ok: false,\n rhizohFailureKind,\n ...(providerHttpMatch ? { providerHttpStatus: Number(providerHttpMatch[1]) } : {}),\n error: code || msg || \"rhizoh_llm_failed\",\n detail: msg || code || undefined,\n reply:\n error?.reply ||\n (status === 500 ? \"Rhizoh bağlantısı geçici olarak kesildi.\" : msg),\n directive: error?.directive || \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n ...stressTaxonomy\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicSeal) {\n try {\n if (!EPISTEMIC_SEAL_SECRET) {\n sendJson(res, 503, { ok: false, error: \"epistemic_seal_disabled\" });\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`epistemic_seal:${ip}`, RL_EPISTEMIC_SEAL_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const truth = body.truth_contract;\n if (!truth || typeof truth !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"truth_contract_required\" });\n return;\n }\n const sealBody = {\n truth_contract: truth,\n runtime_hash: String(body.runtime_hash || \"\"),\n model_route: {\n provider: body.model_route?.provider ?? null,\n model: body.model_route?.model ?? null\n },\n memory_digest: String(body.memory_digest || \"\"),\n world_snapshot_hash: String(body.world_snapshot_hash || \"\"),\n timestamp: Number(body.timestamp) || Date.now()\n };\n const canonical = canonicalEpistemicSealString(sealBody);\n const { hash, signature } = hashAndSignEpistemicSeal(canonical, EPISTEMIC_SEAL_SECRET);\n recordGenesisEpistemicSealIssued(hash);\n const attestation = {\n schema: EPISTEMIC_SEAL_SCHEMA,\n algorithm: \"SHA-256+HMAC-SHA256\",\n keyHint: process.env.CASTLE_EPISTEMIC_SEAL_SECRET ? \"CASTLE_EPISTEMIC_SEAL_SECRET\" : \"CASTLE_GATEWAY_TOKEN\",\n issuedAt: Date.now(),\n gateway: { service: \"castle-gateway\", listenPort: PORT }\n };\n sendJson(res, 200, {\n ok: true,\n hash,\n signature,\n attestation,\n canonicalBytes: Buffer.byteLength(canonical, \"utf8\")\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"seal_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicLogsBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`epistemic_logs:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 120) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const persisted = await persistEpistemicLedgerBatch(auth.uid, entries);\n recordGenesisEpistemicLedgerPersisted(persisted.persisted);\n // Internal cognition layer: forecast stays gateway/ledger-only (no client exposure).\n try {\n await persistEpistemicForecastBatch(auth.uid, persisted.normalized || []);\n } catch {\n /* forecast failures must not break primary ledger ingest */\n }\n sendJson(res, 200, {\n ok: true,\n persisted: persisted.persisted,\n mode: persisted.mode,\n latest: persisted.normalized?.[persisted.normalized.length - 1] || null\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"epistemic_log_ingest_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.authorityLedgerBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`authority_ledger:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 80) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const witnessed = persistAuthorityLedgerWitnessBatchV1(auth.uid, entries, EPISTEMIC_SEAL_SECRET);\n if (witnessed.witnessed > 0) {\n recordGenesisEpistemicLedgerPersisted(witnessed.witnessed);\n const clientSeal = witnessed.lastWitness?.clientSealHash;\n if (clientSeal) recordGenesisEpistemicSealIssued(clientSeal);\n }\n sendJson(res, 200, {\n ok: true,\n witnessed: witnessed.witnessed,\n quarantined: witnessed.quarantined,\n chainHeight: witnessed.chainHeight,\n chainHead: witnessed.chainHead,\n lastWitness: witnessed.lastWitness,\n results: witnessed.results\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"authority_ledger_witness_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.authorityLedgerReplay) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const snapshot = getAuthorityLedgerWitnessSnapshotV1(auth.uid);\n const replay = replayAuthorityLedgerWitnessChainV1(auth.uid);\n sendJson(res, 200, {\n ok: true,\n snapshot,\n replay,\n schema: \"castle.rhizoh.authority_ledger_witness.v1.replay_surface\"\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"authority_ledger_replay_failed\") });\n }\n return;\n }\n\n sendJson(res, 404, { ok: false, error: \"not_found\" });\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\nspiralWssForGenesis = wss;\nconst orchestrator = createOrchestrator();\nconst clientStats = new Map();\nlet broadcasterClientId = null;\nconst spiralState = {\n rooms: [{ id: \"room-default\", name: \"Spiral Default Arena\" }],\n activeByClient: new Map(),\n characters: new Map() // id -> {id,name,role,roomId,source}\n};\n\n/** Castle social rooms — keyed by `castleRoomKey` from {@link WS_MESSAGE.CASTLE_SOCIAL_PULSE}. */\nconst castleSocialRoomState = new Map();\nconst CASTLE_SOCIAL_MEMBER_TTL_MS = 90_000;\nconst CASTLE_SOCIAL_MAX_PULSE_BYTES = 8192;\nconst RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES = 5 * 1024 * 1024;\nconst RHIZOH_VOICE_LIVE_MAX_CHUNKS = 96;\n\n/**\n * Voice Engine v3 live media lane.\n * Client streams MediaRecorder chunks over the existing gateway WS; STOP reuses the\n * gateway ASR runner so orchestration remains the single authority downstream.\n */\nconst rhizohVoiceLiveSessions = new WeakMap();\n\nfunction appendRhizohVoiceLiveEventV0(session, type, payloadRefSeed = \"\") {\n if (!session) return null;\n session.eventSeq = (Number(session.eventSeq) || 0) + 1;\n const event = createRhizohImmutableEventV0({\n type,\n sessionId: session.sessionId,\n traceId: session.traceId,\n actorSeed: session.sessionId || session.traceId,\n eventSeq: session.eventSeq,\n payloadRef: createRhizohPayloadRefV0(payloadRefSeed || `${session.sessionId}:${type}:${session.eventSeq}`)\n });\n session.events = [...(session.events || []), event].slice(-64);\n return event;\n}\n\nfunction sendRhizohVoiceLiveEnvelopeV0(socket, type, payload) {\n try {\n if (socket?.readyState === 1) socket.send(JSON.stringify(createEnvelope(type, payload)));\n } catch {\n /* noop */\n }\n}\n\nfunction decodeRhizohVoiceLiveChunkV0(payload = {}) {\n const b64 = String(payload.audioBase64 || \"\").trim();\n if (!b64) return { ok: false, error: \"audio_base64_required\" };\n try {\n const buf = Buffer.from(b64, \"base64\");\n if (!buf.length) return { ok: false, error: \"audio_decode_empty\" };\n return { ok: true, buffer: buf };\n } catch {\n return { ok: false, error: \"audio_base64_invalid\" };\n }\n}\n\nfunction handleRhizohVoiceLiveStartV0(socket, payload = {}) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const sessionId = String(p.sessionId || `live_${Date.now().toString(36)}`).slice(0, 96);\n rhizohVoiceLiveSessions.set(socket, {\n sessionId,\n traceId: String(p.traceId || \"\").slice(0, 128),\n mimeType: String(p.mimeType || \"audio/webm\").slice(0, 80),\n languageCode: String(p.languageCode || \"tr-TR\").slice(0, 32),\n path: String(p.path || \"both\").slice(0, 24),\n chunks: [],\n chunkIndexes: [],\n totalBytes: 0,\n nextChunkIndex: 1,\n eventSeq: 0,\n events: [],\n startedAtMs: Date.now()\n });\n appendRhizohVoiceLiveEventV0(rhizohVoiceLiveSessions.get(socket), \"VOICE_LIVE_SESSION_STARTED\", `${sessionId}:live_session`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_START, {\n ok: true,\n sessionId,\n schema: \"castle.rhizoh.voice_live.gateway_lane.v0\"\n });\n}\n\nfunction handleRhizohVoiceLiveChunkV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const sequence = validateRhizohVoiceLiveChunkPayloadV0(payload, {\n expectedChunkIndex: session.nextChunkIndex\n });\n if (!sequence.ok) {\n rhizohVoiceLiveSessions.delete(socket);\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:chunk_reject`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: sequence.error,\n expectedChunkIndex: sequence.expectedChunkIndex,\n chunkIndex: sequence.chunkIndex\n });\n return;\n }\n const decoded = decodeRhizohVoiceLiveChunkV0(payload);\n if (!decoded.ok) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: decoded.error\n });\n return;\n }\n if (\n session.chunks.length >= RHIZOH_VOICE_LIVE_MAX_CHUNKS ||\n session.totalBytes + decoded.buffer.length > RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES\n ) {\n rhizohVoiceLiveSessions.delete(socket);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_audio_too_large\"\n });\n return;\n }\n session.chunks.push(decoded.buffer);\n session.chunkIndexes.push(sequence.chunkIndex);\n session.totalBytes += decoded.buffer.length;\n session.nextChunkIndex += 1;\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_CHUNK_ACCEPTED\", `${session.sessionId}:chunk:${sequence.chunkIndex}`);\n}\n\nfunction handleRhizohVoiceLiveStopV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n rhizohVoiceLiveSessions.delete(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const lastChunkIndex = Number(payload?.lastChunkIndex);\n if (Number.isInteger(lastChunkIndex) && lastChunkIndex !== session.chunks.length) {\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:stop_mismatch`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_chunk_count_mismatch\",\n expectedChunkCount: lastChunkIndex,\n receivedChunkCount: session.chunks.length\n });\n return;\n }\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_STOP_RECEIVED\", `${session.sessionId}:stop:${session.chunks.length}`);\n const audio = Buffer.concat(session.chunks);\n if (!audio.length) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"audio_empty\"\n });\n return;\n }\n runRhizohVoiceTranscribeV3({\n path: String(payload?.path || session.path || \"both\"),\n audioBase64: audio.toString(\"base64\"),\n mimeType: String(payload?.mimeType || session.mimeType || \"audio/webm\"),\n languageCode: String(payload?.languageCode || session.languageCode || \"tr-TR\"),\n traceId: String(payload?.traceId || session.traceId || \"\"),\n sessionId: session.sessionId,\n transport: \"gateway_ws_live\"\n })\n .then((result) => {\n appendRhizohVoiceLiveEventV0(session, result?.ok ? \"VOICE_TRANSCRIBE_FINAL\" : \"VOICE_TRANSCRIBE_FAILED\", `${session.sessionId}:final`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_FINAL, {\n ...result,\n sessionId: session.sessionId,\n transportPath: \"gateway_ws_live\",\n bytes: audio.length,\n chunkCount: session.chunks.length,\n chunkSequence: Object.freeze({\n ok: true,\n firstChunkIndex: session.chunkIndexes[0] || null,\n lastChunkIndex: session.chunkIndexes[session.chunkIndexes.length - 1] || null,\n receivedChunkCount: session.chunks.length\n }),\n eventTrace: Object.freeze((session.events || []).slice(-16)),\n recordedMs: Math.max(0, Date.now() - session.startedAtMs)\n });\n })\n .catch((e) => {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_transcribe_failed\",\n detail: String(e?.message || e)\n });\n });\n}\n\nfunction removeCastleSocialClientFromAllRooms(clientId) {\n const cid = String(clientId || \"\");\n if (!cid) return;\n for (const room of castleSocialRoomState.values()) {\n room.members.delete(cid);\n }\n}\n\nfunction buildCastleSocialRoomPayload(roomKey) {\n const room = castleSocialRoomState.get(roomKey);\n if (!room) return null;\n const now = Date.now();\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const roster = [...room.members.values()].map((m) => ({\n userId: m.userId,\n lastMs: m.lastMs,\n ...m.sp\n }));\n const peerFeeds =\n room.walPeers && room.walPeers.size\n ? [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }))\n : [];\n return { castleRoomKey: roomKey, seq: room.seq, roster, peerFeeds, ts: now };\n}\n\nconst CASTLE_WAL_PEER_MAX_FEED_BYTES = 48 * 1024;\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {{ ok: boolean, code?: string, reason?: string }} gate\n */\nfunction rejectWalPeerIngressV0(socket, gate) {\n const code = gate.code || \"wal_peer_rejected\";\n recordWalPeerRejectV0(code);\n recordSubstrateOtelEventV0(\"wal_peer_reject\", { code, reason: String(gate.reason || \"\").slice(0, 120) });\n socket.send(\n JSON.stringify(\n createEnvelope(WS_MESSAGE.ERROR, {\n error: gate.reason || \"WAL peer ingress rejected.\",\n code\n })\n )\n );\n}\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {Record<string, unknown> | null | undefined} walPeerFeed\n * @returns {boolean}\n */\nfunction gateWalPeerIngressV0(socket, walPeerFeed) {\n const authGate = assertWalPeerSocketAuthorityV0(socket);\n if (!authGate.ok) {\n rejectWalPeerIngressV0(socket, authGate);\n return false;\n }\n const sigGate = validateWalPeerFeedSignatureV0(\n walPeerFeed && typeof walPeerFeed === \"object\" ? walPeerFeed : {}\n );\n if (!sigGate.ok) {\n rejectWalPeerIngressV0(socket, sigGate);\n return false;\n }\n return true;\n}\n\nfunction handleCastleWalPeerFeed(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_WAL_PEER_MAX_FEED_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_WAL_PEER_FEED too large.\" })));\n return;\n }\n const walPeerFeed = p.walPeerFeed && typeof p.walPeerFeed === \"object\" ? p.walPeerFeed : {};\n if (!gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const castleRoomKey = String(p.castleRoomKey || \"wal:convergence\").slice(0, 64);\n const castleId = String(p.castleId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId,\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: room.members.get(socket.clientId)?.sp ?? {}\n });\n const peerFeeds = [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }));\n const body = {\n castleRoomKey,\n seq: room.seq,\n peerFeeds,\n ts: now\n };\n const env = createEnvelope(WS_MESSAGE.CASTLE_WAL_PEER_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys(), ...room.walPeers.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction handleCastleSocialPulse(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_SOCIAL_MAX_PULSE_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_SOCIAL_PULSE too large.\" })));\n return;\n }\n const castleRoomKey = String(p.castleRoomKey || \"default\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n const sp = p.socialPulse && typeof p.socialPulse === \"object\" ? p.socialPulse : {};\n const walPeerFeed =\n p.walPeerFeed && typeof p.walPeerFeed === \"object\"\n ? p.walPeerFeed\n : sp.walPeerFeed && typeof sp.walPeerFeed === \"object\"\n ? sp.walPeerFeed\n : null;\n if (walPeerFeed && !gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const spNext = walPeerFeed ? { ...sp, walPeerFeed } : { ...sp };\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: spNext\n });\n if (walPeerFeed) {\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId: String(p.castleId || userId).slice(0, 64),\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n }\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const body = buildCastleSocialRoomPayload(castleRoomKey);\n if (!body) return;\n const env = createEnvelope(WS_MESSAGE.CASTLE_SOCIAL_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcast(msgObj) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcastState() {\n broadcast(\n createEnvelope(WS_MESSAGE.BROADCAST_STATE, {\n broadcasterClientId,\n viewers: [...wss.clients].filter((c) => c.readyState === 1).length\n })\n );\n}\n\nfunction currentSpiralStatePayload() {\n return {\n rooms: spiralState.rooms,\n characters: [...spiralState.characters.values()]\n };\n}\n\nfunction broadcastSpiralState() {\n broadcast(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload()));\n}\n\nfunction ensureRoom(roomId, fallbackName = null) {\n if (!roomId) return spiralState.rooms[0].id;\n const exists = spiralState.rooms.find((r) => r.id === roomId);\n if (exists) return exists.id;\n const created = { id: roomId, name: fallbackName || `Spiral-${roomId.slice(-4)}` };\n spiralState.rooms.push(created);\n return created.id;\n}\n\nfunction syncSpiralCharactersWithWorld(snapshot) {\n const agents = snapshot?.agents || [];\n for (const a of agents) {\n if (!spiralState.characters.has(a.id)) {\n spiralState.characters.set(a.id, {\n id: a.id,\n name: a.id,\n role: \"agent\",\n roomId: spiralState.rooms[0].id,\n source: \"world\"\n });\n }\n }\n}\n\nfunction broadcastExcept(msgObj, exceptSocket) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client !== exceptSocket && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction validateInputFrame(payload, socket) {\n const stats = clientStats.get(socket.clientId) || { lastFrame: 0, windowStart: Date.now(), msgCount: 0 };\n const now = Date.now();\n if (now - stats.windowStart > 5000) {\n stats.windowStart = now;\n stats.msgCount = 0;\n }\n stats.msgCount += 1;\n clientStats.set(socket.clientId, stats);\n if (stats.msgCount > 120) return \"Rate limit exceeded.\";\n\n if (!payload || !Number.isInteger(payload.frame)) return \"frame must be integer.\";\n if (payload.frame <= stats.lastFrame) return \"frame must be monotonic.\";\n const commands = Array.isArray(payload.commands) ? payload.commands : [];\n if (commands.length > 8) return \"Too many commands in one frame.\";\n for (const cmd of commands) {\n if (!cmd || typeof cmd.command !== \"string\") return \"Invalid command format.\";\n if (\n cmd.command !== \"SPAWN_AGENT\" &&\n cmd.command !== \"LIST_CASTLES\" &&\n cmd.command !== \"SPAWN_ENTITY\" &&\n cmd.command !== \"CREATE_CASTLE\"\n ) return \"Unsupported command.\";\n }\n stats.lastFrame = payload.frame;\n clientStats.set(socket.clientId, stats);\n return null;\n}\n\nfunction validateSignalPayload(payload) {\n if (!payload || typeof payload !== \"object\") return \"Invalid signaling payload.\";\n const signalType = payload.signalType;\n if (Object.values(CASTLE_NETWORK_SIGNAL_V0).includes(signalType)) {\n return null;\n }\n if (![\"OFFER\", \"ANSWER\", \"ICE\"].includes(signalType)) return \"Invalid signal type.\";\n if (typeof payload.to !== \"string\" || payload.to.length < 3) return \"Invalid signal destination.\";\n if (signalType !== \"ICE\" && JSON.stringify(payload.sdp || {}).length > 16 * 1024) return \"SDP too large.\";\n if (signalType === \"ICE\" && JSON.stringify(payload.candidate || {}).length > 6 * 1024) return \"ICE too large.\";\n return null;\n}\n\nfunction sanitizeRawMessage(raw) {\n if (!raw) return null;\n const text = raw.toString();\n if (Buffer.byteLength(text, \"utf8\") > MAX_MESSAGE_BYTES) return null;\n return text;\n}\n\nwss.on(\"connection\", async (socket, req) => {\n const origin = req?.headers?.origin || \"\";\n if (!httpCors.isWebSocketOriginAllowed(origin)) {\n socket.close(1008, \"Origin not allowed\");\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const path = req?.url || \"\";\n const query = new URL(`ws://localhost${path}`).searchParams;\n const token = query.get(\"token\");\n if (!token || token !== REQUIRED_GATEWAY_TOKEN) {\n socket.close(1008, \"Invalid gateway token\");\n return;\n }\n }\n if (REQUIRE_AUTH) {\n const result = await verifyClientToken(req);\n if (!result.ok) {\n const isLocal = origin.includes(\"localhost\") || origin.includes(\"127.0.0.1\") || !origin;\n if (!(ALLOW_DEV_ANON && isLocal)) {\n socket.close(1008, result.reason);\n return;\n }\n socket.auth = { ok: true, kind: \"dev-anon\", user: { uid: \"dev-anon\" } };\n } else {\n socket.auth = result;\n }\n }\n\n socket.clientId = `c-${Math.random().toString(36).slice(2, 8)}`;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.HELLO, { message: \"Castle Gateway online.\", clientId: socket.clientId })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.WORLD_SNAPSHOT, orchestrator.worldSnapshot())));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.BROADCAST_STATE, { broadcasterClientId, viewers: [...wss.clients].filter((c) => c.readyState === 1).length })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload())));\n broadcastExcept(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }), socket);\n\n socket.on(\"message\", (raw) => {\n const safeRaw = sanitizeRawMessage(raw);\n if (!safeRaw) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Message too large or invalid.\" })));\n return;\n }\n const parsed = safeJsonParse(safeRaw);\n if (!parsed?.type) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid message.\" })));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND_TEXT) {\n const canonical = parseTextToCommand(parsed.payload?.text);\n if (!canonical) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Unknown command text.\" })));\n return;\n }\n const result = orchestrator.applyCommand(canonical);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND) {\n const result = orchestrator.applyCommand(parsed.payload);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n }\n\n if (parsed.type === WS_MESSAGE.INPUT_FRAME) {\n const err = validateInputFrame(parsed.payload, socket);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const frame = parsed.payload.frame;\n const commands = Array.isArray(parsed.payload?.commands) ? parsed.payload.commands : [];\n orchestrator.queueInputFrame(socket.clientId, frame, commands);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, queued: true, frame })));\n }\n\n if (parsed.type === WS_MESSAGE.SIGNAL) {\n const payload = parsed.payload || {};\n if (Object.values(CASTLE_NETWORK_SIGNAL_V0).includes(payload.signalType)) {\n const netErr = validateCastleNetworkSignalPayloadV0(socket, payload);\n if (netErr) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: netErr })));\n return;\n }\n handleCastleNetworkSignalV0(socket, payload, wss);\n return;\n }\n const err = validateSignalPayload(payload);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const signalType = payload.signalType;\n const to = payload.to;\n for (const client of wss.clients) {\n if (client.clientId === to && client.readyState === 1) {\n client.send(JSON.stringify(createEnvelope(WS_MESSAGE.SIGNAL, { ...payload, from: socket.clientId })));\n break;\n }\n }\n }\n\n if (parsed.type === WS_MESSAGE.DRONE_TELEMETRY) {\n const payload = parsed.payload || {};\n const json = JSON.stringify(payload);\n if (Buffer.byteLength(json, \"utf8\") > 48 * 1024) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"DRONE_TELEMETRY payload too large.\" })));\n return;\n }\n broadcastExcept(createEnvelope(WS_MESSAGE.DRONE_TELEMETRY, { ...payload, relayFrom: socket.clientId }), socket);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_START) {\n handleRhizohVoiceLiveStartV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_CHUNK) {\n handleRhizohVoiceLiveChunkV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_STOP) {\n handleRhizohVoiceLiveStopV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_SOCIAL_PULSE) {\n handleCastleSocialPulse(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_WAL_PEER_FEED) {\n handleCastleWalPeerFeed(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.OPEN_DATA_QUERY) {\n queryOpenData(parsed.payload)\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: true, result })));\n })\n .catch((e) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: false, error: e.message })));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_QUERY) {\n runRhizohBrainV2({\n ...(parsed.payload || {}),\n uid: socket?.auth?.user?.uid || \"unknown\"\n })\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, result)));\n })\n .catch(async () => {\n const fallback = await runRhizohBrain(parsed.payload || {}).catch(() => ({\n ok: false,\n reply: \"Rhizoh temporary disturbance. Habitat remains stable.\"\n }));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, fallback)));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.BROADCAST_REGISTER) {\n const role = String(parsed.payload?.role || \"\");\n if (role === \"GENESIS_BROADCAST_AGENT\") {\n broadcasterClientId = socket.clientId;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, registered: role })));\n broadcastState();\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.STUDIO_CUE) {\n // Only registered broadcaster can push global cues.\n if (!broadcasterClientId || socket.clientId !== broadcasterClientId) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Only broadcaster agent can emit STUDIO_CUE.\" })));\n return;\n }\n const cue = parsed.payload || {};\n if (typeof cue !== \"object\") {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid STUDIO_CUE payload.\" })));\n return;\n }\n broadcast(createEnvelope(WS_MESSAGE.STUDIO_CUE, cue));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_JOIN_ROOM) {\n const requested = String(parsed.payload?.roomId || \"\").trim();\n const requestedName = String(parsed.payload?.roomName || \"\").trim();\n const roomId = ensureRoom(requested || `room-${Math.random().toString(36).slice(2, 7)}`, requestedName || null);\n spiralState.activeByClient.set(socket.clientId, roomId);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, roomId })));\n broadcastSpiralState();\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_CREATE_CHARACTER) {\n const name = String(parsed.payload?.name || \"\").trim().slice(0, 32) || `AI-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;\n const role = String(parsed.payload?.role || \"agent\").trim().slice(0, 24) || \"agent\";\n const roomId = ensureRoom(\n String(parsed.payload?.roomId || spiralState.activeByClient.get(socket.clientId) || spiralState.rooms[0].id),\n null\n );\n\n // Character creation always maps to an authoritative world agent.\n const result = orchestrator.applyCommand({\n command: COMMAND.SPAWN_AGENT,\n params: { targetCastleId: \"castle-01\" }\n });\n if (!result?.ok || !result?.data?.id) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Character creation failed.\" })));\n return;\n }\n const id = result.data.id;\n spiralState.characters.set(id, { id, name, role, roomId, source: \"spawned\" });\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, characterId: id, roomId })));\n broadcastSpiralState();\n return;\n }\n });\n\n socket.on(\"close\", () => {\n if (socket.clientId === broadcasterClientId) broadcasterClientId = null;\n clientStats.delete(socket.clientId);\n rhizohVoiceLiveSessions.delete(socket);\n spiralState.activeByClient.delete(socket.clientId);\n removeCastleSocialClientFromAllRooms(socket.clientId);\n removeCastleNetworkClientV0(socket.clientId, wss);\n broadcast(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }));\n broadcastState();\n });\n});\n\nsetInterval(() => {\n void (async () => {\n try {\n const { frame, snapshot, delta, packedDelta } = orchestrator.tick(1 / 20);\n syncSpiralCharactersWithWorld(snapshot);\n broadcast(createEnvelope(WS_MESSAGE.NETWORK_TICK, { frame }));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_TICK, snapshot));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA, delta));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA_PACKED, packedDelta));\n broadcastSpiralState();\n await runAcademyEventTick();\n const executed = await tickAutomations(Date.now());\n for (const a of executed) {\n await addTranscript(a.uid, {\n source: \"planner\",\n eventType: \"automation-executed\",\n text: `Automation executed: ${a.title} (${a.action})`,\n roomId: \"studio-main\",\n meta: { automationId: a.id, action: a.action, deviceId: a.deviceId || \"\" }\n });\n }\n } catch (e) {\n console.error(\"[GATEWAY] world tick / automations loop error:\", String(e?.message || e));\n }\n })();\n}, 50);\n\n(async () => {\n await rhizohRuntime.telemetry.initOpenTelemetry();\n if (String(process.env.CASTLE_GENESIS_DISK_PERSIST ?? \"\").trim() === \"\") {\n process.env.CASTLE_GENESIS_DISK_PERSIST = \"1\";\n }\n const genesisDiskBoot = await hydrateGenesisContinuityPersistenceBootV0();\n if (!genesisDiskBoot.ok) {\n console.warn(\"[GATEWAY] genesis disk hydrate failed; continuing with in-memory genesis continuity\");\n }\n httpServer.listen(PORT, () => {\n startGenesisCanonicalClock();\n installGenesisCheckpointSurfaceGetter(buildGenesisRuntimeSurfacePayloadLive);\n setGenesisContinuityAfterPublishHook((seq) => {\n noteGenesisCheckpointSeqCommitted(seq);\n noteGenesisContinuitySeqAcceptedV0(seq);\n });\n startGenesisContinuityInfraSampler(buildGenesisRuntimeSurfacePayloadLive, 2000);\n logProductionObservatorySurfaceGuardsV0();\n logGatewaySubstrateAuthorityGuardsV0();\n if (isLlmWorkerEnabledV0()) {\n try {\n const workerBoot = initLlmWorkerAtBootV0();\n console.log(\"[GATEWAY] llm worker boot:\", workerBoot);\n } catch (workerErr) {\n console.error(\"[GATEWAY] llm worker boot failed:\", workerErr);\n }\n }\n console.log(`[GATEWAY] ws/http://localhost:${PORT}`);\n console.log(\n `[GATEWAY] genesis continuity: GET ${rhizohRuntime.routes.genesisRuntime} | POST ${rhizohRuntime.routes.genesisIngress} | SSE ${rhizohRuntime.routes.genesisStream} | replay ${rhizohRuntime.routes.genesisReplay} | diff ${rhizohRuntime.routes.genesisReplayDiff} | equiv ${rhizohRuntime.routes.genesisReplayEquivalence} | analytics ${rhizohRuntime.routes.genesisReplayAnalytics} | evolution ${rhizohRuntime.routes.genesisReplayEvolution}`\n );\n if (String(process.env.CASTLE_GENESIS_OBSERVATORY_LIVE_V01 ?? \"\").trim() === \"1\") {\n console.log(\"GENESIS OBSERVATORY LIVE v0.1 — READ ONLY PHASE STARTED\");\n }\n });\n})().catch((e) => {\n console.error(\"[GATEWAY] fatal boot (before or during listen):\", e);\n process.exit(1);\n});\n"}, "ce0942265f5d5a171efa6873525baae5162155c6": {"t": 1785754131.9697218, "n": 174502, "h": "e68f994d7a3c44f0", "c": "import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport dotenv from \"dotenv\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconsole.log(\"🔥 [GENESIS_BOOT] entry file:\", __filename);\nconsole.log(\"🔥 [GENESIS_BOOT] node env:\", process.env.NODE_ENV);\nconsole.log(\"🔥 [GENESIS_BOOT] pid:\", process.pid);\nconsole.log(\"🧬 [GENESIS_BOOT] cwd:\", process.cwd());\nconsole.log(\"🧬 [GENESIS_BOOT] import.meta.url:\", import.meta.url);\ndotenv.config({ path: path.join(__dirname, \"..\", \".env\") });\ndotenv.config({ path: path.join(__dirname, \"..\", \".env.local\"), override: true });\n\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport { createServer } from \"node:http\";\nimport { createOrchestrator } from \"@castle/orchestrator\";\nimport { parseTextToCommand } from \"@castle/command-dsl\";\nimport {\n WS_MESSAGE,\n COMMAND,\n createEnvelope,\n createRhizohImmutableEventV0,\n createRhizohPayloadRefV0,\n safeJsonParse,\n validateRhizohVoiceLiveChunkPayloadV0\n} from \"@castle/protocol\";\nimport { queryOpenData } from \"./openData.js\";\nimport { verifyClientToken } from \"./auth.js\";\nimport { runRhizohBrain } from \"./rhizohBrain.js\";\nimport { queryRhizohLlm } from \"./rhizohLlmGateway.js\";\nimport { rhizohGatewayTurn } from \"./rhizohGatewayTurn.js\";\nimport {\n enqueueLlmWorkerTaskAsyncV0,\n getLatestLlmWorkerTaskIdV0,\n getLlmWorkerDebugSnapshotV0,\n getLlmWorkerTaskSnapshotV0,\n initLlmWorkerAtBootV0,\n isLlmWorkerAsyncEnabledV0,\n isLlmWorkerEnabledV0,\n runLlmWorkerTaskV0\n} from \"./llmWorkerRuntime.js\";\nimport {\n RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n runRhizohVoiceTranscribeV3,\n rhizohVoiceTranscribeEnvV3\n} from \"./rhizohVoiceTranscribeV3.js\";\nimport { createHttpCorsPolicy, normalizeHttpOrigin, parseAllowedOriginsFromEnv } from \"./httpCorsPolicyV1.js\";\nimport {\n applyLanguagePropagationToLlmPayloadV1,\n applyLanguagePropagationToVoiceBodyV1,\n buildRhizohLanguagePropagationEchoV1,\n parseRhizohLanguagePropagationV1\n} from \"./rhizohLanguagePropagationV1.js\";\nimport { trimRhizohLlmGatewayPayloadV1 } from \"./rhizohLlmPayloadTrimV1.js\";\nimport {\n meshAppendDelta,\n meshContinuityAggregate,\n meshJoin,\n meshLeave,\n meshReplayFromNodeId,\n meshReplayFromSeq,\n meshSnapshot,\n meshSubscribeSse\n} from \"./presenceRoomMesh.js\";\nimport { runRhizohBrainV2 } from \"./rhizohBrainV2.js\";\nimport {\n listConnections,\n createConnection,\n updateConnection,\n deleteConnection,\n setDefaultConnection,\n resolveConnection\n} from \"./llmConnectionsStore.js\";\nimport { getCloudSyncSnapshotV0, mergeCloudSyncSnapshotV0 } from \"./castleCloudSyncStore.js\";\nimport {\n CASTLE_NETWORK_SIGNAL_V0,\n handleCastleNetworkSignalV0,\n listCastleNetworkPresenceV0,\n removeCastleNetworkClientV0,\n validateCastleNetworkSignalPayloadV0\n} from \"./castleNetworkRelayV0.js\";\nimport { checkHttpRateLimit, getHttpClientIp } from \"./castleHttpRateLimit.js\";\nimport {\n submitAbuseReportV0,\n listModerationQueueV0,\n softBlockUserV0,\n removeSoftBlockV0,\n listSoftBlockedUsersV0\n} from \"./ops/moderationMvpV0.js\";\nimport { listRecentAgentSnapshotsV0 } from \"./ops/agentObservabilityV0.js\";\nimport { readAgentContainmentConfigV0 } from \"./ops/agentContainmentV0.js\";\nimport { readCostContainmentConfigV0, getCostContainmentStatsV0 } from \"./ops/costContainmentV0.js\";\nimport { getGlobalCostLedgerSnapshotV0, getGclBackendStatusV0 } from \"./ops/globalCostLedgerV0.js\";\nimport { evaluateEconomicStrategyL3V0 } from \"./ops/economicStrategyEngineL3V0.js\";\nimport { getPhasedRolloutStatsV0, readPhasedRolloutTierV0 } from \"./ops/phasedRolloutV0.js\";\nimport { buildUnifiedStateNarrativeV0 } from \"./ops/unifiedStateNarrativeV0.js\";\nimport { resolveHardeningTenantIdV0 } from \"./ops/narrativeTenantIsolationV0.js\";\nimport {\n classifyStressResponseV0,\n verifyStressTaxonomyCoverageV0,\n verifyStressConflictResolutionV0\n} from \"./ops/stressResponseTaxonomyV0.js\";\nimport { verifyResolutionStabilityEnvelopeV0 } from \"./ops/resolutionStabilityEnvelopeV0.js\";\nimport { logLlmAccess } from \"./castleLlmAudit.js\";\nimport { appendMemory, listMemories, getMemoryContext, getPersonaGoalMemory, setPersonaGoalMemory, autoCompactMemories } from \"./memoryStore.js\";\nimport { getFirebasePersistence } from \"./firebasePersistence.js\";\nimport { registerAgentIdentity, listAgentIdentities, getAgentIdentity, updateAgentIdentity } from \"./agentIdentityStore.js\";\nimport { queueAcademyEvent, listAcademyEvents, resolveAcademyEvent, runAcademyEventTick } from \"./academyEventEngine.js\";\nimport { enqueuePdfJob, getPdfJob } from \"./pdfPipeline.js\";\nimport {\n createPublishSession,\n listPublishSessions,\n updatePublishSession,\n addTranscript,\n listTranscripts,\n searchMetadata,\n registerDevice,\n listDevices,\n updateDevice,\n appendTelemetry,\n listTelemetry,\n createAutomation,\n listAutomations,\n tickAutomations,\n enqueueCommand,\n listCommands,\n updateCommandStatus,\n registerSocialChannel,\n listSocialChannels\n} from \"./studioOpsStore.js\";\nimport { metrics as infraMetrics } from \"./infra/metrics.js\";\nimport { renderRhizohPrometheusMetrics, rhizohEnterpriseMetrics } from \"./infra/rhizohEnterpriseMetrics.js\";\nimport { scoreHealth } from \"./infra/healthScore.js\";\nimport { getTraceById, listTracesBySession } from \"./infra/traceRegistry.js\";\nimport { canonicalEpistemicSealString, hashAndSignEpistemicSeal, EPISTEMIC_SEAL_SCHEMA } from \"./epistemicSeal.js\";\nimport { persistEpistemicLedgerBatch } from \"./epistemicLedgerStore.js\";\nimport { persistEpistemicForecastBatch } from \"./epistemicForecastStore.js\";\nimport { persistAuthorityLedgerWitnessBatchV1 } from \"./authorityLedgerWitnessStoreV1.js\";\nimport { buildRhizohExternalGroundTruthPayload } from \"./rhizohExternalGroundTruthGateway.js\";\nimport { ingestRhizohExternalLossBatchHttp } from \"./rhizohExternalLossIngestGateway.js\";\nimport {\n ingestRhizohProductOutcomeHttp,\n listRhizohProductOutcomeAggregates,\n verifyRhizohOutcomeHmac,\n verifyRhizohOutcomeSourceToken\n} from \"./rhizohProductOutcomeIngestGateway.js\";\nimport { initRhizoh } from \"./rhizohProductionBootstrap.js\";\nimport { handleAcademicObservatoryExportGetV0 } from \"./rhizoh/academicObservatoryHttpV0.js\";\nimport { handleCouncilAnomalyReasoningPostV0 } from \"./council/rhizohEpistemicCouncilHttpV0.js\";\nimport {\n handleLiveSportsBundleGetV0,\n handleLiveNewsHeadlinesGetV0,\n handleLiveWorldFeedGetV0\n} from \"./rhizoh/liveWorldFeedHttpV0.js\";\nimport {\n buildGenesisRuntimeSurfacePayload,\n recordGenesisEpistemicLedgerPersisted,\n recordGenesisEpistemicSealIssued,\n startGenesisCanonicalClock\n} from \"./genesisRuntimeSurfaceV0.js\";\nimport { registerGenesisSseClient, getGenesisContinuitySeq, setGenesisContinuityAfterPublishHook } from \"./genesisContinuityStreamHubV0.js\";\nimport { computeGenesisReplayFingerprintV0 } from \"./genesisReplayFingerprintV0.js\";\nimport { buildGenesisGatewayCapabilitiesV0 } from \"./genesisGatewayCapabilitiesV0.js\";\nimport {\n installGenesisCheckpointSurfaceGetter,\n noteGenesisCheckpointSeqCommitted,\n getLatestGenesisSignedCheckpoint\n} from \"./genesisContinuityCheckpointV0.js\";\nimport { hydrateGenesisContinuityPersistenceBootV0 } from \"./genesisContinuityHydrateBootV0.js\";\nimport { startGenesisContinuityInfraSampler } from \"./genesisContinuityInfraSamplerV0.js\";\nimport {\n genesisCheckpointQueryBySeqV0,\n genesisCheckpointQueryRangeV0,\n genesisCheckpointQueryLineageV0\n} from \"./genesisCheckpointQueryV0.js\";\nimport {\n genesisContinuityEventArchiveEnabled,\n queryGenesisContinuityEventArchiveV0,\n GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION\n} from \"./genesisContinuityEventArchiveV0.js\";\nimport { resolveGenesisReplayRouterV1, GENESIS_REPLAY_ROUTER_SCHEMA } from \"./genesisReplayRouterV1.js\";\nimport {\n computeGenesisReplayTemporalDiffV1,\n GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA\n} from \"./genesisReplayTemporalDiffV1.js\";\nimport { compareGenesisReplayEquivalenceV1 } from \"./genesisReplayEquivalenceV1.js\";\nimport { computeGenesisReplayAnalyticsV1 } from \"./genesisReplayAnalyticsV1.js\";\nimport { computeGenesisReplayEvolutionV1 } from \"./genesisReplayEvolutionV1.js\";\nimport { handleStripeCheckoutCreate, handleStripeMembershipWebhook } from \"./stripeWebhookMembership.js\";\nimport { buildRealityHealthPayload } from \"./edgeRealityConsistencyV1.js\";\nimport {\n assertWalPeerSocketAuthorityV0,\n buildGatewaySubstrateAuthoritySnapshotV0,\n logGatewaySubstrateAuthorityGuardsV0,\n validateWalPeerFeedSignatureV0\n} from \"./gatewaySubstrateAuthorityV0.js\";\nimport {\n buildSubstrateOperationalSnapshotV0,\n recordWalPeerRejectV0,\n renderSubstratePrometheusMetricsV0\n} from \"./infra/substrateOperationalMetrics.js\";\nimport { ingestClientSubstrateHealthV0 } from \"./rhizohSubstrateHealthIngestV0.js\";\nimport { recordSubstrateOtelEventV0 } from \"./infra/opentelemetrySubstrateV0.js\";\nimport {\n GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n getGenesisWorldObservationIngressStatusV0,\n ingestGenesisWorldObservationV0\n} from \"./genesisContinuityClientIngressV0.js\";\nimport {\n getGenesisContinuitySeqAuditV0,\n noteGenesisContinuitySeqAcceptedV0\n} from \"./genesisContinuitySeqGapV0.js\";\n\n/** Render / Fly / Railway: `PORT` — yoksa `CASTLE_GATEWAY_PORT` — yoksa 8090. */\nconst PORT =\n Number(process.env.PORT) ||\n Number(process.env.CASTLE_GATEWAY_PORT) ||\n 8090;\n/** Spiral / world WebSocket server — set after `wss` init; used by genesis runtime surface. */\nlet spiralWssForGenesis = null;\nconst MAX_MESSAGE_BYTES = Number(process.env.CASTLE_MAX_MESSAGE_BYTES || 32 * 1024);\nconst REQUIRED_GATEWAY_TOKEN = process.env.CASTLE_GATEWAY_TOKEN || \"\";\nconst ALLOWED_ORIGINS = parseAllowedOriginsFromEnv().fromList;\nconst httpCors = createHttpCorsPolicy();\n\n/** @returns {string|null} `Access-Control-Allow-Origin`; null = browser CORS fail. */\nfunction accessControlAllowOriginValue(req) {\n return httpCors.accessControlAllowOriginValue(req);\n}\n\nfunction applyHttpCorsHeaders(req, res) {\n return httpCors.applyHttpCorsHeaders(req, res);\n}\nconst REQUIRE_AUTH = process.env.CASTLE_REQUIRE_AUTH === \"true\";\nconst ALLOW_DEV_ANON = process.env.CASTLE_ALLOW_DEV_ANON !== \"false\";\nconst ALLOW_DEV_HTTP_UID = process.env.CASTLE_ALLOW_DEV_HTTP_UID !== \"false\";\n/** Presence mesh: allow `X-Castle-Guest-Id` when Firebase auth absent (default on). Set `false` to require auth only. */\nconst ALLOW_MESH_GUEST = process.env.CASTLE_ALLOW_MESH_GUEST !== \"false\";\nconst SOCIAL_RETRY_MAX = Math.max(0, Math.min(5, Number(process.env.CASTLE_SOCIAL_RETRY_MAX || 2)));\nconst SOCIAL_RETRY_BASE_MS = Math.max(100, Number(process.env.CASTLE_SOCIAL_RETRY_BASE_MS || 700));\nconst TELEGRAM_BOT_TOKEN = process.env.CASTLE_TELEGRAM_BOT_TOKEN || \"\";\nconst WHATSAPP_TOKEN = process.env.CASTLE_WHATSAPP_TOKEN || \"\";\nconst WHATSAPP_PHONE_NUMBER_ID = process.env.CASTLE_WHATSAPP_PHONE_NUMBER_ID || \"\";\nconst RL_RHIZOH_LLM_PER_MIN = Math.max(5, Number(process.env.CASTLE_RL_RHIZOH_LLM_PER_MIN || 40));\nconst RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN || 30)\n);\nconst RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN || 120));\nconst RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN = Math.max(\n 10,\n Number(process.env.CASTLE_RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN || 60)\n);\nconst RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN = Math.max(8, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN || 48));\nconst RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN = Math.max(4, Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN || 24));\nconst RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN || 18)\n);\nconst RL_EPISTEMIC_SEAL_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_EPISTEMIC_SEAL_PER_MIN || 120));\nconst RL_EPISTEMIC_LOG_PER_MIN = Math.max(20, Number(process.env.CASTLE_RL_EPISTEMIC_LOG_PER_MIN || 240));\nconst EPISTEMIC_SEAL_SECRET = String(\n process.env.CASTLE_EPISTEMIC_SEAL_SECRET || process.env.CASTLE_GATEWAY_TOKEN || \"\"\n).trim();\nconst RL_LLM_CONN_TEST_PER_MIN = Math.max(2, Number(process.env.CASTLE_RL_LLM_CONNECTION_TEST_PER_MIN || 8));\n\n/** One-shot production warnings: CORS surface, auth toggle, dev bypass flags (observatory deploy hygiene). */\nfunction logProductionObservatorySurfaceGuardsV0() {\n if (String(process.env.NODE_ENV || \"\").toLowerCase() !== \"production\") return;\n const tag = \"[GATEWAY_PROD_GUARD]\";\n const primaryCors = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n const corsListEmpty = ALLOWED_ORIGINS.length === 0;\n if (corsListEmpty && !primaryCors) {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty and CASTLE_HTTP_CORS_ORIGIN empty — production still merges RHIZOH_BROWSER_ORIGINS_V1 defaults; set env for preview/staging hosts`\n );\n } else if (corsListEmpty && primaryCors && primaryCors !== \"*\") {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty — only CASTLE_HTTP_CORS_ORIGIN set; add web.app + firebaseapp.com (and previews) to CASTLE_ALLOWED_ORIGINS if clients use alternate hostnames`\n );\n }\n if (primaryCors && /localhost|127\\.0\\.0\\.1/i.test(primaryCors)) {\n console.warn(`${tag} CORS: CASTLE_HTTP_CORS_ORIGIN is localhost while NODE_ENV=production`);\n }\n if (REQUIRE_AUTH) {\n console.warn(\n `${tag} AUTH: CASTLE_REQUIRE_AUTH=true — clients must send a valid gateway credential (otherwise silent 401 on replay/SSE); EventSource cannot set custom headers unless proxied`\n );\n if (REQUIRED_GATEWAY_TOKEN.length < 16) {\n console.warn(`${tag} AUTH: CASTLE_GATEWAY_TOKEN missing or short while require-auth enabled`);\n }\n }\n if (ALLOW_DEV_ANON) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_ANON is not \"false\" — dev anonymous path may be live in production (set CASTLE_ALLOW_DEV_ANON=false)`\n );\n }\n if (ALLOW_DEV_HTTP_UID) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_HTTP_UID is not \"false\" — X-Castle-Dev-Uid bypass risk (set CASTLE_ALLOW_DEV_HTTP_UID=false)`\n );\n }\n}\n\nconst rhizohRuntime = initRhizoh();\n\n/** İstemci: llmKeySource veya keySource — env | user_connection | auto */\nfunction normalizeClientLlmKeySource(raw) {\n const s = String(raw ?? \"\").trim().toLowerCase();\n if (s === \"env\" || s === \"server\") return \"env\";\n if (s === \"user_connection\" || s === \"connection\" || s === \"user\") return \"user_connection\";\n if (s === \"auto\" || s === \"\") return \"auto\";\n return null;\n}\n\n/**\n * Server-side provider wins for env/auto unless user_connection — client bundle may still send provider=openai.\n * @param {\"env\"|\"user_connection\"|\"auto\"} keyMode\n * @param {{ provider?: string, model?: string, apiKey?: string } | null} conn\n * @param {{ provider?: string, model?: string }} payload\n */\nfunction resolveServerLlmProviderV0(keyMode, conn, payload) {\n const envProvider = String(process.env.CASTLE_LLM_PROVIDER || \"\").trim() || undefined;\n const envModel = String(process.env.CASTLE_LLM_MODEL || \"\").trim() || undefined;\n\n if (keyMode === \"user_connection\") {\n return {\n resolvedProvider: conn?.provider || payload?.provider,\n resolvedModel: conn?.model || payload?.model,\n connApiKey: conn?.apiKey || \"\"\n };\n }\n\n if (keyMode === \"env\") {\n return {\n resolvedProvider: envProvider ?? payload?.provider,\n resolvedModel: envModel ?? payload?.model,\n connApiKey: \"\"\n };\n }\n\n if (conn?.provider && conn?.apiKey) {\n return {\n resolvedProvider: conn.provider,\n resolvedModel: conn.model,\n connApiKey: conn.apiKey\n };\n }\n\n return {\n resolvedProvider: envProvider ?? payload?.provider,\n resolvedModel: envModel ?? payload?.model,\n connApiKey: \"\"\n };\n}\n\nfunction jsonReplacerSafeV0(_key, value) {\n if (typeof value === \"bigint\") return Number(value);\n return value;\n}\n\nfunction sendJson(res, statusCode, payload) {\n res.writeHead(statusCode, { \"Content-Type\": \"application/json; charset=utf-8\" });\n try {\n res.end(JSON.stringify(payload, jsonReplacerSafeV0));\n } catch (error) {\n res.end(\n JSON.stringify({\n ok: false,\n error: \"response_serialize_failed\",\n detail: String(error?.message || error)\n })\n );\n }\n}\n\nfunction logRhizohHealth(stage, detail = {}) {\n try {\n console.info(`[CASTLE_${String(stage || \"unknown\")}]`, detail && typeof detail === \"object\" ? detail : {});\n } catch {\n /* noop */\n }\n}\n\nfunction renderInfraMetricsProm() {\n const lines = [\n `castle_gateway_events_processed_total ${infraMetrics.eventsProcessed}`,\n `castle_gateway_duplicate_reject_total ${infraMetrics.duplicateRejects}`,\n `castle_gateway_enqueue_latency_ms ${infraMetrics.enqueueLatencyMs}`,\n `castle_gateway_queue_depth ${infraMetrics.queueDepth}`,\n `castle_gateway_queue_lag_seconds ${(Number(infraMetrics.queueLag || 0) / 1000).toFixed(3)}`,\n `castle_gateway_errors_total ${infraMetrics.errors}`\n ];\n const buckets = infraMetrics.enqueueLatencyBuckets || {};\n for (const k of Object.keys(buckets)) {\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"${k}\"} ${buckets[k]}`);\n }\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"+Inf\"} ${infraMetrics.eventsProcessed}`);\n if (process.env.CASTLE_SUBSTRATE_METRICS !== \"0\") {\n lines.push(renderSubstratePrometheusMetricsV0().trimEnd());\n }\n return lines.join(\"\\n\");\n}\n\nasync function fetchJsonWithTimeout(url, timeoutMs = 1200) {\n const ctl = new AbortController();\n const tid = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const res = await fetch(url, { signal: ctl.signal });\n if (!res.ok) return null;\n return await res.json();\n } catch {\n return null;\n } finally {\n clearTimeout(tid);\n }\n}\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction getHttpPathname(req) {\n let p = String(req.url || \"\").split(\"?\")[0];\n /* Reverse proxies / clients sometimes append `/` — avoid silent 404 on e.g. `/rhizoh/genesis/stream/`. */\n while (p.length > 1 && p.endsWith(\"/\")) {\n p = p.slice(0, -1);\n }\n return p;\n}\n\nfunction rhizohLlmEnvConfigured() {\n const p = String(process.env.CASTLE_LLM_PROVIDER || \"openai\").toLowerCase();\n const key =\n p === \"anthropic\"\n ? process.env.ANTHROPIC_API_KEY\n : p === \"gemini\"\n ? process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY\n : p === \"xai\"\n ? process.env.XAI_API_KEY\n : p === \"deepseek\"\n ? process.env.DEEPSEEK_API_KEY\n : p === \"mistral\"\n ? process.env.MISTRAL_API_KEY\n : p === \"openrouter\"\n ? process.env.OPENROUTER_API_KEY\n : process.env.OPENAI_API_KEY;\n return !!String(key || \"\").trim();\n}\n\n/**\n * @param {import(\"firebase-admin/firestore\").Firestore | null} db\n * @param {number} timeoutMs\n */\nasync function probeFirestoreReachable(db, timeoutMs = 2000) {\n if (!db) return { ok: false, reason: \"no_db\" };\n const timeout = sleep(timeoutMs).then(() => ({ ok: false, reason: \"timeout\" }));\n const work = db\n .collection(\"_castle_gateway_health\")\n .doc(\"ping\")\n .get()\n .then(() => ({ ok: true, reason: \"reachable\" }))\n .catch((e) => ({ ok: false, reason: String(e?.message || e || \"firestore_error\") }));\n return Promise.race([work, timeout]);\n}\n\nfunction stripHtml(text = \"\") {\n return String(text)\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \" \")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \" \")\n .replace(/<[^>]+>/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction haversineMeters(aLat, aLon, bLat, bLon) {\n const R = 6371000;\n const dLat = ((bLat - aLat) * Math.PI) / 180;\n const dLon = ((bLon - aLon) * Math.PI) / 180;\n const sa = Math.sin(dLat / 2);\n const sb = Math.sin(dLon / 2);\n const x = sa * sa + Math.cos((aLat * Math.PI) / 180) * Math.cos((bLat * Math.PI) / 180) * sb * sb;\n return 2 * R * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));\n}\n\nasync function checkPublishTargetHealth(protocol, target) {\n const p = String(protocol || \"\").toUpperCase();\n const t = String(target || \"\").trim();\n if (!t) return { ok: false, reason: \"target_missing\", shouldReconnect: false };\n if (p === \"RTMP\") {\n const ok = t.startsWith(\"rtmp://\") || t.startsWith(\"rtmps://\");\n return { ok, reason: ok ? \"rtmp_target_format_ok\" : \"invalid_rtmp_target\", shouldReconnect: !ok, reconnectInSec: 15 };\n }\n if (p === \"WHIP\" || p === \"SFU\") {\n try {\n const res = await fetch(t, { method: \"OPTIONS\" });\n const ok = res.status < 500;\n return { ok, reason: `http_${res.status}`, shouldReconnect: !ok, reconnectInSec: 8 };\n } catch {\n return { ok: false, reason: \"network_unreachable\", shouldReconnect: true, reconnectInSec: 8 };\n }\n }\n return { ok: true, reason: \"unchecked_protocol\", shouldReconnect: false };\n}\n\nasync function fetchTextFromUrl(url) {\n const res = await fetch(url, { method: \"GET\" });\n if (!res.ok) throw new Error(`fetch_${res.status}`);\n const ct = String(res.headers.get(\"content-type\") || \"\");\n if (ct.includes(\"application/pdf\")) throw new Error(\"pdf_binary_not_supported_directly\");\n const body = await res.text();\n return stripHtml(body).slice(0, 14000);\n}\n\nfunction buildTaskActionGraph(task = \"\", context = {}) {\n const t = String(task || \"\").toLowerCase();\n if (t.includes(\"dinner\") || t.includes(\"akşam yemeği\") || t.includes(\"aksam yemegi\")) {\n return {\n taskTitle: \"Dinner Prep Planner\",\n nodes: [\n { id: \"n1\", title: \"Kitchen preheat\", action: \"smart-oven.preheat\", params: { temperatureC: 190, mode: \"fan\" } },\n { id: \"n2\", title: \"Lighting comfort\", action: \"smart-light.set_scene\", params: { scene: \"warm_dinner\" } },\n { id: \"n3\", title: \"Fridge inventory check\", action: \"smart-fridge.inventory_scan\", params: {} },\n { id: \"n4\", title: \"Reminder notification\", action: \"smart-watch.notify\", params: { text: \"Dinner prep started.\" } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" },\n { from: \"n3\", to: \"n4\" }\n ]\n };\n }\n return {\n taskTitle: task || \"General Planner\",\n nodes: [\n { id: \"n1\", title: \"Safety check\", action: \"device.safety_check\", params: { context } },\n { id: \"n2\", title: \"Primary action\", action: \"device.execute_task\", params: { task } },\n { id: \"n3\", title: \"Report\", action: \"studio.timeline_note\", params: { task } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" }\n ]\n };\n}\n\nfunction buildEthicsPrograms() {\n return {\n policyVersion: \"v1\",\n tracks: [\n {\n id: \"elderly-care-assist\",\n audience: \"elderly\",\n title: \"Elderly Assistance Track\",\n principles: [\"human-in-the-loop\", \"consent-first\", \"low-friction alerts\"],\n modules: [\"medication-reminder\", \"fall-risk-check\", \"family-notify\", \"daily-companion-brief\"]\n },\n {\n id: \"kids-safe-learning\",\n audience: \"children\",\n title: \"Kids Safe Learning Track\",\n principles: [\"guardian-consent\", \"age-appropriate-language\", \"content-filtering\"],\n modules: [\"study-coach\", \"screen-time-balance\", \"safe-social-share\", \"emergency-contact\"]\n }\n ]\n };\n}\n\nasync function deliverViaTelegram(channel, text) {\n const chatId = String(channel.endpoint || \"\").trim();\n if (!TELEGRAM_BOT_TOKEN) throw new Error(\"telegram_token_missing\");\n if (!chatId) throw new Error(\"telegram_chat_id_missing\");\n const url = `https://api.telegram.org/bot${encodeURIComponent(TELEGRAM_BOT_TOKEN)}/sendMessage`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ chat_id: chatId, text })\n });\n if (!res.ok) throw new Error(`telegram_http_${res.status}`);\n return { provider: \"telegram\", status: res.status };\n}\n\nasync function deliverViaWhatsapp(channel, text) {\n const to = String(channel.endpoint || \"\").trim();\n if (!WHATSAPP_TOKEN) throw new Error(\"whatsapp_token_missing\");\n if (!WHATSAPP_PHONE_NUMBER_ID) throw new Error(\"whatsapp_phone_number_id_missing\");\n if (!to) throw new Error(\"whatsapp_to_missing\");\n const url = `https://graph.facebook.com/v22.0/${encodeURIComponent(WHATSAPP_PHONE_NUMBER_ID)}/messages`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${WHATSAPP_TOKEN}` },\n body: JSON.stringify({ messaging_product: \"whatsapp\", to, type: \"text\", text: { body: text } })\n });\n if (!res.ok) throw new Error(`whatsapp_http_${res.status}`);\n return { provider: \"whatsapp\", status: res.status };\n}\n\nasync function deliverViaWebhook(channel, text) {\n const endpoint = String(channel.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"webhook_endpoint_missing\");\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text, platform: channel.platform || \"webhook\", ts: Date.now() })\n });\n if (!res.ok) throw new Error(`webhook_http_${res.status}`);\n return { provider: \"webhook\", status: res.status };\n}\n\nasync function deliverSocialMessage(channel, text) {\n const platform = String(channel.platform || \"webhook\").toLowerCase();\n if (platform === \"telegram\") return deliverViaTelegram(channel, text);\n if (platform === \"whatsapp\") return deliverViaWhatsapp(channel, text);\n return deliverViaWebhook(channel, text);\n}\n\nasync function deliverSocialWithRetry(channel, text) {\n let attempt = 0;\n let lastError = null;\n while (attempt <= SOCIAL_RETRY_MAX) {\n try {\n const result = await deliverSocialMessage(channel, text);\n return { ok: true, attempt: attempt + 1, ...result };\n } catch (error) {\n lastError = error;\n if (attempt >= SOCIAL_RETRY_MAX) break;\n const jitter = Math.floor(Math.random() * 90);\n await sleep(SOCIAL_RETRY_BASE_MS * Math.pow(2, attempt) + jitter);\n }\n attempt += 1;\n }\n return { ok: false, attempt: attempt + 1, error: String(lastError?.message || \"delivery_failed\") };\n}\n\nasync function dispatchCommandToAdapter(device, command) {\n const adapter = String(command?.adapter || device?.adapter || \"websocket\").toLowerCase();\n const endpoint = String(device?.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"device_endpoint_missing\");\n if (adapter === \"mqtt\") {\n // MQTT delivery is proxied via a bridge endpoint (HTTP webhook style).\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n type: \"mqtt-command\",\n topic: `castle/devices/${device.id}/commands`,\n action: command.action,\n params: command.params || {}\n })\n });\n if (!res.ok) throw new Error(`mqtt_bridge_http_${res.status}`);\n return { ok: true, adapter, via: \"http-bridge\", status: res.status };\n }\n if (!endpoint.startsWith(\"ws://\") && !endpoint.startsWith(\"wss://\")) throw new Error(\"invalid_websocket_endpoint\");\n const sendResult = await new Promise((resolve, reject) => {\n const ws = new WebSocket(endpoint);\n const timer = setTimeout(() => {\n try {\n ws.close();\n } catch {\n /* noop */\n }\n reject(new Error(\"websocket_timeout\"));\n }, 3000);\n ws.on(\"open\", () => {\n ws.send(\n JSON.stringify({\n type: \"castle-command\",\n commandId: command.id,\n deviceId: command.deviceId,\n action: command.action,\n params: command.params || {}\n }),\n (err) => {\n clearTimeout(timer);\n ws.close();\n if (err) reject(new Error(\"websocket_send_failed\"));\n else resolve({ ok: true, adapter: \"websocket\", via: \"ws\" });\n }\n );\n });\n ws.on(\"error\", () => {\n clearTimeout(timer);\n reject(new Error(\"websocket_connect_failed\"));\n });\n });\n return sendResult;\n}\n\nfunction readHttpJson(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => {\n if (!buf) return resolve({});\n const payload = safeJsonParse(buf);\n if (!payload || typeof payload !== \"object\") return reject(new Error(\"invalid_json_payload\"));\n resolve(payload);\n });\n req.on(\"error\", reject);\n });\n}\n\n/** Ham gövde (HMAC doğrulaması için). */\nfunction readHttpBodyText(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => resolve(buf));\n req.on(\"error\", reject);\n });\n}\n\nfunction maskOpaqueId(value) {\n const t = String(value || \"\");\n if (!t) return \"(empty)\";\n if (t.length <= 8) return \"****\";\n return `${t.slice(0, 4)}…${t.slice(-2)}`;\n}\n\nfunction readGatewayHttpToken(req) {\n const x = String(req.headers?.[\"x-castle-gateway-token\"] || \"\").trim();\n if (x) return x;\n const auth = String(req.headers?.authorization || \"\");\n const m = /^Bearer\\s+(.+)$/i.exec(auth);\n return m ? String(m[1] || \"\").trim() : \"\";\n}\n\nasync function resolveHttpUser(req) {\n const authResult = await verifyClientToken(req);\n if (authResult?.ok && authResult.user?.uid) {\n return { ok: true, uid: String(authResult.user.uid), mode: authResult.kind || \"auth\" };\n }\n if (ALLOW_DEV_ANON && ALLOW_DEV_HTTP_UID) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) return { ok: true, uid: `dev-${devUid.slice(0, 64)}`, mode: \"dev-header\" };\n }\n return { ok: false, reason: authResult?.reason || \"auth_required\" };\n}\n\n/**\n * Epistemic ledger ingest — Firebase uid OR valid transport token + X-Castle-Dev-Uid.\n * Prod disables CASTLE_ALLOW_DEV_HTTP_UID; LLM may still work while telemetry 401'd without this.\n */\nasync function resolveEpistemicIngestActor(req) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok && tok === REQUIRED_GATEWAY_TOKEN) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) {\n return { ok: true, uid: `gw-telemetry-${devUid.slice(0, 64)}`, mode: \"gateway-token-dev-uid\" };\n }\n return { ok: false, reason: \"dev_uid_required\" };\n }\n if (tok) {\n return { ok: false, reason: \"gateway_token_mismatch\" };\n }\n }\n const auth = await resolveHttpUser(req);\n if (auth.ok) return auth;\n return { ok: false, reason: auth.reason || \"auth_required\" };\n}\n\n/** Mesh actor: Firebase uid, dev header, or `guest:<uuid>` via header / `guestId` query (SSE cannot set headers). */\nasync function resolveMeshActor(req) {\n const auth = await resolveHttpUser(req);\n if (auth.ok) return { ok: true, clientUid: auth.uid, mode: auth.mode };\n if (ALLOW_MESH_GUEST) {\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const gid = String(req.headers?.[\"x-castle-guest-id\"] || q.searchParams.get(\"guestId\") || \"\").trim();\n if (gid) {\n const normalized = gid.startsWith(\"guest:\") ? gid.slice(0, 180) : `guest:${gid.slice(0, 160)}`;\n return { ok: true, clientUid: normalized, mode: \"guest-header\" };\n }\n }\n return { ok: false, reason: \"mesh_auth_required\" };\n}\n\n/** Live genesis runtime JSON (GET /rhizoh/genesis/runtime + SSE infra sampler). */\nasync function buildGenesisRuntimeSurfacePayloadLive() {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n let spiralWsActive = 0;\n if (spiralWssForGenesis) {\n for (const c of spiralWssForGenesis.clients) {\n if (c.readyState === 1) spiralWsActive += 1;\n }\n }\n const base = buildGenesisRuntimeSurfacePayload({\n infraMetrics,\n scoredHealth: scored,\n rhizohEnterpriseMetrics,\n mesh: meshContinuityAggregate(),\n workerHealth: worker,\n port: PORT,\n spiralWebSocketClientsActive: spiralWsActive\n });\n return {\n ...base,\n replayFingerprint: computeGenesisReplayFingerprintV0(base),\n gatewayCapabilities: buildGenesisGatewayCapabilitiesV0(),\n genesisStream: {\n schema: \"castle.genesis.stream_cursor.v0\",\n lastAcceptedSeq: getGenesisContinuitySeq(),\n clientIngress: {\n schema: GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n contract: \"castle.world_observation.ingress_envelope.v1\",\n postPath: rhizohRuntime.routes.genesisIngress,\n closure: getGenesisWorldObservationIngressStatusV0()\n },\n seqContinuity: getGenesisContinuitySeqAuditV0(),\n eventArchive: genesisContinuityEventArchiveEnabled()\n ? {\n enabled: true,\n projection: GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION,\n queryPath: rhizohRuntime.routes.genesisContinuityEvents\n }\n : { enabled: false },\n replayRouter: {\n schema: GENESIS_REPLAY_ROUTER_SCHEMA,\n queryPath: rhizohRuntime.routes.genesisReplay,\n queryHint: \"from=&to= or range=from-to; optional type=&checkpoints=0\",\n temporalDiffPath: rhizohRuntime.routes.genesisReplayDiff,\n temporalDiffSchema: GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA,\n equivalencePath: rhizohRuntime.routes.genesisReplayEquivalence,\n analyticsPath: rhizohRuntime.routes.genesisReplayAnalytics,\n evolutionPath: rhizohRuntime.routes.genesisReplayEvolution\n }\n }\n };\n}\n\nconst httpServer = createServer(async (req, res) => {\n const corsAllow = applyHttpCorsHeaders(req, res);\n if (req.method === \"OPTIONS\") {\n if (!corsAllow && req.headers?.origin) {\n console.warn(\"[GATEWAY_CORS] preflight rejected for origin:\", req.headers.origin);\n }\n res.writeHead(corsAllow ? 204 : 403);\n res.end();\n return;\n }\n\n const pathname = getHttpPathname(req);\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/genesis/__ping\") {\n sendJson(res, 200, {\n ok: true,\n pid: process.pid,\n entry: __filename,\n time: Date.now(),\n genesis: true\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/epistemic/__probe\") {\n sendJson(res, 200, {\n ok: true,\n epistemicIngestActorV2: true,\n routes: {\n seal: rhizohRuntime.routes.epistemicSeal,\n logsBatch: rhizohRuntime.routes.epistemicLogsBatch,\n authorityLedgerBatch: rhizohRuntime.routes.authorityLedgerBatch\n },\n hasGatewayToken: Boolean(REQUIRED_GATEWAY_TOKEN),\n gatewayTokenLen: REQUIRED_GATEWAY_TOKEN ? REQUIRED_GATEWAY_TOKEN.length : 0\n });\n return;\n }\n\n if (\n req.method === \"GET\" &&\n (pathname === \"/rhizoh/live/world-feed\" ||\n pathname === \"/rhizoh/live/sports-bundle\" ||\n pathname === \"/rhizoh/live/news-headlines\")\n ) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_live_world_feed:${ip}`, RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const out =\n pathname === \"/rhizoh/live/sports-bundle\"\n ? await handleLiveSportsBundleGetV0()\n : pathname === \"/rhizoh/live/news-headlines\"\n ? await handleLiveNewsHeadlinesGetV0(req)\n : await handleLiveWorldFeedGetV0(req);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/webhooks/stripe\") {\n await handleStripeMembershipWebhook(req, res);\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/billing/stripe/checkout\") {\n await handleStripeCheckoutCreate(req, res, { readHttpJson, resolveHttpUser, sendJson });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/live\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, live: false, dns: true, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n sendJson(res, 200, { ok: true, live: true, dns: true, service: \"castle-gateway\", ts: Date.now() });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/cors\") {\n const reqOrigin = normalizeHttpOrigin(req.headers?.origin);\n const allowOrigin = accessControlAllowOriginValue(req);\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n requestOrigin: reqOrigin || null,\n allowed: Boolean(allowOrigin),\n allowOrigin: allowOrigin || null,\n allowSetSize: httpCors.allowSet.size\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/ready\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, ready: false, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n try {\n const persistence = getFirebasePersistence();\n const llmProbe = rhizohLlmEnvConfigured();\n sendJson(res, 200, {\n ok: true,\n ready: true,\n dns: true,\n persistence: persistence.mode,\n service: \"castle-gateway\",\n gateway: \"ok\",\n llm: llmProbe ? \"ok\" : \"degraded\",\n spine: llmProbe ? \"ok\" : \"partial\"\n });\n } catch (e) {\n sendJson(res, 503, { ok: false, ready: false, reason: String(e?.message || e), service: \"castle-gateway\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/reality\") {\n const payload = buildRealityHealthPayload();\n sendJson(res, payload.ok ? 200 : 503, payload);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/substrate\") {\n const authority = buildGatewaySubstrateAuthoritySnapshotV0();\n const operational = buildSubstrateOperationalSnapshotV0();\n const drift = buildRealityHealthPayload();\n const ok = Boolean(authority.productionAuthoritySatisfied) && drift.ok;\n sendJson(res, ok ? 200 : 503, {\n ok,\n service: \"castle-gateway\",\n ts: Date.now(),\n authority,\n operational,\n drift: { ok: drift.ok, errors: drift.drift?.errors, warnings: drift.drift?.warnings }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.councilAnomalyReasoning) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_council_anomaly:${ip}`, 30, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const body = await readHttpJson(req);\n const out = handleCouncilAnomalyReasoningPostV0(body);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/substrate/health\") {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const body = await readHttpJson(req);\n const uid = (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestClientSubstrateHealthV0(body, { uid, clientId: uid });\n if (!result.ok) {\n sendJson(res, 400, result);\n return;\n }\n recordSubstrateOtelEventV0(\"client_health_ingest\", { uid: String(uid).slice(0, 32) });\n sendJson(res, 200, result);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/deps\") {\n const t0 = Date.now();\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, {\n ok: false,\n dns: true,\n llm: false,\n firestore: false,\n phase: \"maintenance\",\n latencyMs: Date.now() - t0,\n service: \"castle-gateway\"\n });\n return;\n }\n const persistence = getFirebasePersistence();\n let firestoreOk = persistence.mode !== \"firebase\";\n let firestoreDetail = persistence.mode === \"file\" ? \"file_mode\" : \"not_applicable\";\n if (persistence.mode === \"firebase\" && persistence.db) {\n const pr = await probeFirestoreReachable(persistence.db, 2000);\n firestoreOk = pr.ok;\n firestoreDetail = pr.reason || (pr.ok ? \"reachable\" : \"fail\");\n } else if (persistence.mode === \"firebase\" && !persistence.db) {\n firestoreOk = false;\n firestoreDetail = \"firebase_not_initialized\";\n }\n const llmOk = rhizohLlmEnvConfigured();\n const overallOk = llmOk && firestoreOk;\n const latencyMs = Date.now() - t0;\n sendJson(res, 200, {\n ok: overallOk,\n dns: true,\n llm: llmOk,\n firestore: persistence.mode === \"firebase\" ? firestoreOk : false,\n persistence: persistence.mode,\n firestoreDetail,\n latencyMs,\n service: \"castle-gateway\",\n wsPort: PORT\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health\") {\n const persistence = getFirebasePersistence();\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n wsPort: PORT,\n persistence: persistence.mode,\n presenceMesh: \"sse\"\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/health\") {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n sendJson(res, 200, {\n ok: true,\n role: \"gateway\",\n queueLagMs: infraMetrics.queueLag,\n queueDepth: infraMetrics.queueDepth,\n errors: infraMetrics.errors,\n status: scored.status,\n score: scored.score,\n reasons: scored.reasons,\n worker: worker\n ? {\n status: worker.status,\n score: worker.score,\n reasons: worker.reasons\n }\n : null,\n timestamp: Date.now()\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisRuntime) {\n const payload = await buildGenesisRuntimeSurfacePayloadLive();\n sendJson(res, 200, payload);\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.genesisIngress) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const actor = await resolveMeshActor(req);\n const clientId = actor.ok ? actor.clientUid : (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestGenesisWorldObservationV0(body, { clientId });\n if (!result.ok) {\n const status =\n result.error === \"rate_limited\" || result.error === \"agent_spoke_throttled\"\n ? 429\n : result.error === \"payload_too_large\"\n ? 413\n : 400;\n sendJson(res, status, result);\n return;\n }\n sendJson(res, result.idempotent ? 200 : 202, result);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisStream) {\n const corsOrigin = accessControlAllowOriginValue(req);\n if (!corsOrigin && String(process.env.NODE_ENV || \"\").toLowerCase() === \"production\") {\n const og = String(req.headers?.origin || \"\").trim();\n if (og) {\n console.warn(\"[GATEWAY] genesis SSE: missing Access-Control-Allow-Origin for browser Origin (CORS whitelist mismatch):\", og);\n }\n }\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n res.write(\"retry: 3000\\n\\n\");\n res.write(\": genesis continuity stream\\n\\n\");\n res.write(\"event: genesis\\n\");\n res.write(`data: {\"booted\":true,\"pid\":${process.pid}}\\n\\n`);\n registerGenesisSseClient(res, req);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLatest) {\n const cp = getLatestGenesisSignedCheckpoint();\n if (!cp) {\n sendJson(res, 404, { ok: false, error: \"no_checkpoint_yet\", hint: \"wait_for_seq_interval_or_configure_signing_secret\" });\n return;\n }\n sendJson(res, 200, { ok: true, checkpoint: cp });\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(rhizohRuntime.routes.genesisCheckpointBySeqPrefix)) {\n const tail = pathname.slice(rhizohRuntime.routes.genesisCheckpointBySeqPrefix.length).split(\"/\")[0];\n const seq = parseInt(String(tail || \"\").trim(), 10);\n const out = await genesisCheckpointQueryBySeqV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"checkpoint_not_found\") {\n sendJson(res, 404, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointRange) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryRangeV0(from, to);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"range_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLineage) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const seq = parseInt(String(u.searchParams.get(\"seq\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryLineageV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"lineage_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisContinuityEvents) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const limit = parseInt(String(u.searchParams.get(\"limit\") || \"\").trim(), 10);\n const out = await queryGenesisContinuityEventArchiveV0(from, to, type, limit);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\" || out.error === \"event_archive_disabled\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"archive_file_too_large\") {\n sendJson(res, 413, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplay) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await resolveGenesisReplayRouterV1({\n from: fromSeq,\n to: toSeq,\n type,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayDiff) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const out = await computeGenesisReplayTemporalDiffV1({ from: fromSeq, to: toSeq, type });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEquivalence) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from1 = parseInt(String(u.searchParams.get(\"from1\") || \"\").trim(), 10);\n const to1 = parseInt(String(u.searchParams.get(\"to1\") || \"\").trim(), 10);\n const from2 = parseInt(String(u.searchParams.get(\"from2\") || \"\").trim(), 10);\n const to2 = parseInt(String(u.searchParams.get(\"to2\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await compareGenesisReplayEquivalenceV1({\n from1,\n to1,\n from2,\n to2,\n type,\n includeCheckpoints\n });\n if (\n !out.ok &&\n (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")\n ) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayAnalytics) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayAnalyticsV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEvolution) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const collapseWindows = parseInt(String(u.searchParams.get(\"collapseWindows\") || \"0\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayEvolutionV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n collapseWindows,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/metrics\") {\n res.writeHead(200, { \"Content-Type\": \"text/plain; version=0.0.4; charset=utf-8\" });\n res.end(renderInfraMetricsProm());\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.infraPrometheusMetrics) {\n if (process.env.CASTLE_PROMETHEUS_METRICS !== \"1\") {\n sendJson(res, 404, { ok: false, error: \"prometheus_metrics_disabled\" });\n return;\n }\n res.writeHead(200, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(renderRhizohPrometheusMetrics());\n return;\n }\n if (req.method === \"GET\" && pathname.startsWith(\"/infra/traces/\")) {\n const traceId = decodeURIComponent(pathname.slice(\"/infra/traces/\".length));\n const item = await getTraceById(traceId);\n if (!item) return sendJson(res, 404, { ok: false, error: \"trace_not_found\" });\n return sendJson(res, 200, { ok: true, trace: item });\n }\n if (req.method === \"GET\" && pathname === \"/infra/traces\") {\n const u = new URL(String(req.url || \"/infra/traces\"), `http://${req.headers.host || \"localhost\"}`);\n const sessionId = String(u.searchParams.get(\"sessionId\") || \"\").trim();\n const items = sessionId ? await listTracesBySession(sessionId) : [];\n return sendJson(res, 200, { ok: true, sessionId, traces: items });\n }\n\n /** Presence room mesh (C): JOIN / LEAVE / SNAPSHOT / DELTA / REPLAY (HTTP); SUBSCRIBE = SSE stream. */\n if (req.method === \"POST\" && pathname === \"/presence/mesh/join\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid, clientUid: actor.clientUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/leave\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshLeave(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/snapshot\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n sendJson(res, 200, meshSnapshot(roomUid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/delta\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, MAX_MESSAGE_BYTES);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const seq = meshAppendDelta(roomUid, actor.clientUid, {\n node: body?.node,\n projectionPatch: body?.projectionPatch,\n writerSubject: body?.writerSubject\n });\n sendJson(res, 200, { ok: true, roomUid, seq });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/replay\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const fromNodeId = body?.fromNodeId != null ? String(body.fromNodeId) : \"\";\n const fromSeq = body?.fromSeq;\n if (fromNodeId) {\n sendJson(res, 200, meshReplayFromNodeId(roomUid, fromNodeId));\n return;\n }\n sendJson(res, 200, meshReplayFromSeq(roomUid, fromSeq));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/presence/mesh/subscribe\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const roomUid = String(q.searchParams.get(\"roomUid\") || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n const write = (chunk) => {\n try {\n res.write(chunk);\n } catch {\n /* closed */\n }\n };\n const unsub = meshSubscribeSse(roomUid, write);\n const onClose = () => {\n unsub();\n meshLeave(roomUid, actor.clientUid);\n try {\n res.end();\n } catch {\n /* ignore */\n }\n };\n req.on(\"close\", onClose);\n req.on(\"aborted\", onClose);\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, uid: auth.uid, mode: auth.mode, items: await listConnections(auth.uid) });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const id = await createConnection(auth.uid, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"create_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n await updateConnection(auth.uid, id, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"update_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"DELETE\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n await deleteConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"delete_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url?.startsWith(\"/rhizoh/network/presence\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const u = new URL(req.url, \"http://localhost\");\n const roomKey = String(u.searchParams.get(\"room\") || \"world_space_c2c_v0\").slice(0, 64);\n sendJson(res, 200, {\n ok: true,\n roomKey,\n presence: listCastleNetworkPresenceV0(roomKey)\n });\n } catch (error) {\n sendJson(res, 500, { ok: false, error: error?.message || \"presence_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/rhizoh/sync/vault\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const snapshot = await getCloudSyncSnapshotV0(auth.uid);\n sendJson(res, 200, { ok: true, uid: auth.uid, snapshot });\n } catch (error) {\n sendJson(res, 500, { ok: false, error: error?.message || \"cloud_sync_pull_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/rhizoh/sync/vault\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const snapshot = await mergeCloudSyncSnapshotV0(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, uid: auth.uid, snapshot });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"cloud_sync_push_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections/test\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n if (!checkHttpRateLimit(`llm_conn_test:${auth.uid}`, RL_LLM_CONN_TEST_PER_MIN, 60_000)) {\n return sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n }\n const hasBodyKey = !!String(payload?.apiKey || \"\").trim();\n const result = await queryRhizohLlm(\n {\n message: String(payload?.message || \"Ping from Castle Gateway\"),\n context: payload?.context || { source: \"connection_test\" },\n provider: payload?.provider,\n model: payload?.model,\n apiKey: payload?.apiKey || \"\"\n },\n { allowExternalApiKey: true, llmKeySource: hasBodyKey ? \"auto\" : \"env\" }\n );\n logLlmAccess({\n route: \"/llm/connections/test\",\n uid: auth.uid,\n llmKeyBillingOwner: result.llmKeyBillingOwner,\n llmKeyOrigin: result.llmKeyOrigin,\n provider: result.provider,\n model: result.model,\n connectionId: null\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"connection_test_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAgentIdentities(auth.uid);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerAgentIdentity({\n ownerUid: auth.uid,\n agentId: payload?.agentId,\n role: payload?.role,\n personaSeed: payload?.personaSeed || {},\n capabilityLevel: payload?.capabilityLevel\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/agents/identities/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const owns = await getAgentIdentity(id);\n if (!owns || owns.ownerUid !== auth.uid) return sendJson(res, 403, { ok: false, error: \"forbidden\" });\n const payload = await readHttpJson(req);\n const row = await updateAgentIdentity(id, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/academy/events\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/academy/events\"}`);\n const status = String(url.searchParams.get(\"status\") || \"\");\n const items = await listAcademyEvents({ ownerUid: auth.uid, status, limit: 120 });\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/academy/events\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const ev = await queueAcademyEvent({\n type: payload?.type,\n roomId: payload?.roomId,\n ownerUid: auth.uid,\n participants: payload?.participants || [],\n topic: payload?.topic || \"academy\",\n payload: payload?.payload || {}\n });\n sendJson(res, 200, { ok: true, event: ev });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && String(req.url || \"\").startsWith(\"/academy/events/\") && String(req.url || \"\").endsWith(\"/resolve\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const parts = String(req.url || \"\").split(\"/\").filter(Boolean);\n const eventId = parts[2] || \"\";\n try {\n const payload = await readHttpJson(req);\n const out = await resolveAcademyEvent(eventId, payload || {});\n sendJson(res, 200, { ok: true, event: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_resolve_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/memory\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/memory\"}`);\n const scope = url.searchParams.get(\"scope\") === \"agents\" ? \"agents\" : \"users\";\n const id = String(url.searchParams.get(\"id\") || (scope === \"agents\" ? \"\" : auth.uid));\n const kind = String(url.searchParams.get(\"kind\") || \"\");\n const limit = Number(url.searchParams.get(\"limit\") || 40);\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n sendJson(res, 200, { ok: true, scope, id, kind, items: await listMemories({ scope, id, kind, limit }) });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const profile = await getPersonaGoalMemory(auth.uid);\n sendJson(res, 200, { ok: true, profile });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const profile = await setPersonaGoalMemory({\n uid: auth.uid,\n personaPatch: payload?.personaPatch || {},\n goals: Array.isArray(payload?.goals) ? payload.goals : null,\n preferencesPatch: payload?.preferencesPatch || {}\n });\n sendJson(res, 200, { ok: true, profile });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"profile_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/compact\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const out = await autoCompactMemories({ scope, id });\n sendJson(res, 200, { ok: true, scope, id, ...out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_compact_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/context\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const context = await getMemoryContext({\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n query: String(payload?.query || \"\"),\n limit: Number(payload?.limit || 80)\n });\n sendJson(res, 200, { ok: true, context });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_context_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/place-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const placeName = String(payload?.placeName || \"\").trim();\n if (!placeName) return sendJson(res, 400, { ok: false, error: \"placeName_required\" });\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const persona = String(payload?.waypointPersona || payload?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(placeName)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Place: ${placeName}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Give a concise conversational briefing about this place for a walking-route companion.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-place-brief\", placeName, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `PLACE_BRIEF:${placeName} => ${String(summary.reply || \"\").slice(0, 900)}`,\n tags: [\"place\", \"brief\", \"event-layer\"],\n importance: 0.62,\n meta: { placeName }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"place-brief\",\n text: `${placeName} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: {\n placeName,\n directive: summary.directive,\n lat: hit?.lat ? Number(hit.lat) : null,\n lon: hit?.lon ? Number(hit.lon) : null\n }\n });\n sendJson(res, 200, { ok: true, place: hit || null, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"place_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/route-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const waypoints = Array.isArray(payload?.waypoints) ? payload.waypoints.slice(0, 24) : [];\n const out = [];\n for (const wp of waypoints) {\n const name = String(wp?.name || \"\").trim();\n if (!name) continue;\n const persona = String(wp?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(name)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Waypoint: ${name}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Generate a concise spoken briefing for route waypoint.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-route-brief\", waypoint: name, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n out.push({\n name,\n persona,\n place: hit || null,\n reply: summary.reply,\n directive: summary.directive\n });\n }\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `ROUTE_BRIEF: waypoints=${out.length} ${out.map((x) => `${x.name}:${x.persona}`).join(\" | \")}`.slice(0, 1300),\n tags: [\"route\", \"waypoint\", \"brief\", \"event-layer\"],\n importance: 0.74,\n meta: { count: out.length }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"route-brief\",\n text: out.map((x) => `${x.name}:${String(x.reply || \"\").slice(0, 140)}`).join(\" | \").slice(0, 1400),\n roomId: \"studio-main\",\n meta: {\n waypointCount: out.length,\n waypoints: out.map((x) => ({\n name: x.name,\n persona: x.persona,\n lat: x.place?.lat ? Number(x.place.lat) : null,\n lon: x.place?.lon ? Number(x.place.lon) : null\n }))\n }\n });\n sendJson(res, 200, { ok: true, items: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"route_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/pdf-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const title = String(payload?.title || \"document\");\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const providedText = String(payload?.text || \"\");\n const url = String(payload?.url || \"\");\n let content = providedText;\n if (!content && url) content = await fetchTextFromUrl(url);\n if (!content) return sendJson(res, 400, { ok: false, error: \"text_or_supported_url_required\" });\n const summary = await queryRhizohLlm(\n {\n message: `Summarize this document and provide key points + practical actions.\\nTitle:${title}\\nContent:\\n${content.slice(0, 12000)}`,\n context: { source: \"event-layer-pdf-brief\", title },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `DOC_BRIEF:${title} => ${String(summary.reply || \"\").slice(0, 1000)}`,\n tags: [\"document\", \"brief\", \"library\", \"event-layer\"],\n importance: 0.7,\n meta: { title, url: url || null }\n });\n await addTranscript(auth.uid, {\n source: \"library-brief\",\n eventType: \"pdf-brief\",\n text: `${title} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { title }\n });\n sendJson(res, 200, { ok: true, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/pdf/upload\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req, 8 * 1024 * 1024);\n const job = await enqueuePdfJob({\n uid: auth.uid,\n fileName: payload?.fileName || \"document.pdf\",\n bytesBase64: payload?.contentBase64\n });\n sendJson(res, 200, { ok: true, job });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_upload_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/session\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createPublishSession(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session\",\n text: `Publish session armed: ${row.protocol} ${row.target}`.slice(0, 500),\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: row.id, bridge: row.bridge }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/studio/publish/sessions\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listPublishSessions(auth.uid, 80);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/health-check\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const protocol = String(payload?.protocol || \"WHIP\");\n const target = String(payload?.target || \"\");\n const health = await checkPublishTargetHealth(protocol, target);\n if (payload?.sessionId) {\n await updatePublishSession(auth.uid, String(payload.sessionId), {\n status: health.ok ? \"healthy\" : \"degraded\",\n reconnectPolicy: {\n enabled: true,\n backoffSec: [2, 5, 10, 20],\n nextRetryInSec: health.reconnectInSec || 0,\n maxRetries: 8,\n reason: health.reason\n }\n });\n }\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-health\",\n text: `${protocol} ${target} => ${health.ok ? \"healthy\" : \"degraded\"} (${health.reason})`,\n roomId: \"studio-main\",\n meta: { protocol, target, reconnect: !!health.shouldReconnect }\n });\n sendJson(res, 200, { ok: true, health });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_health_check_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/studio/publish/sessions/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updatePublishSession(auth.uid, id, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session-update\",\n text: `Session ${id} -> ${row.status || \"updated\"}`,\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: id, status: row.status, ingestUrl: row.ingestUrl || \"\" }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/transcripts\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/transcripts\"}`);\n const limit = Math.max(1, Math.min(200, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await listTranscripts(auth.uid, limit);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/transcripts\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await addTranscript(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"transcript_write_failed\" });\n }\n return;\n }\n\n /** E1: broadcast intent → GreenRoom capability → transcript + memory artifact */\n if (req.method === \"POST\" && req.url === \"/studio/capabilities/greenroom\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const traceId = String(payload?.traceId || `TRC-${Date.now().toString(36).toUpperCase()}`);\n const title = String(payload?.title || \"Castle Live Broadcast\").slice(0, 200);\n const intentRaw = String(payload?.intentRaw || \"\").slice(0, 800);\n const audienceEstimate = Math.max(0, Math.min(50_000_000, Number(payload?.audienceEstimate) || 1200));\n const roomId = String(payload?.roomId || \"greenroom-main\");\n const transcriptRow = await addTranscript(auth.uid, {\n source: \"rhizoh\",\n eventType: \"BROADCAST_ROUTED\",\n text: `GreenRoom · ${title}${intentRaw ? `\\nIntent: ${intentRaw}` : \"\"}`,\n roomId,\n meta: {\n ack: \"BROADCAST_ROUTED\",\n capability: \"greenroom\",\n intentType: \"broadcast\",\n traceId,\n audienceEstimate,\n artifact: {\n kind: \"broadcast_card\",\n title,\n traceId,\n roomId,\n createdAt: Date.now()\n }\n }\n });\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n text: `GREENROOM_BROADCAST:${title} trace=${traceId}`,\n tags: [\"broadcast\", \"greenroom\", \"artifact\", \"castle-library\"],\n importance: 0.72,\n kind: \"episodic\",\n meta: { traceId, capability: \"greenroom\", transcriptId: transcriptRow.id }\n });\n sendJson(res, 200, {\n ok: true,\n ack: \"BROADCAST_ROUTED\",\n traceId,\n transcript: transcriptRow,\n replayPath: `/replay/${encodeURIComponent(traceId)}`,\n sharePath: `/replay/${encodeURIComponent(traceId)}`\n });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"greenroom_capability_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/metadata/search\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/metadata/search\"}`);\n const q = String(url.searchParams.get(\"q\") || \"\");\n const limit = Math.max(1, Math.min(300, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await searchMetadata(auth.uid, q, limit);\n sendJson(res, 200, { ok: true, q, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerDevice(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, device: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"device_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const lat = Number(payload?.lat);\n const lon = Number(payload?.lon);\n const speed = Math.max(0, Number(payload?.speed || 0));\n const cfgCenterLat = Number(process.env.CASTLE_GEOFENCE_CENTER_LAT || 41.0082);\n const cfgCenterLon = Number(process.env.CASTLE_GEOFENCE_CENTER_LON || 28.9784);\n const cfgRadiusM = Math.max(100, Number(process.env.CASTLE_GEOFENCE_RADIUS_M || 55000));\n const cfgSpeedCap = Math.max(1, Number(process.env.CASTLE_SPEED_CAP_MPS || 35));\n const dist = Number.isFinite(lat) && Number.isFinite(lon) ? haversineMeters(cfgCenterLat, cfgCenterLon, lat, lon) : 0;\n const geofenceOk = !Number.isFinite(dist) ? true : dist <= cfgRadiusM;\n const speedOk = speed <= cfgSpeedCap;\n const failsafe = !(geofenceOk && speedOk);\n const safety = {\n geofenceOk,\n speedOk,\n failsafe,\n distanceFromCenterM: Number.isFinite(dist) ? dist : null,\n speedCapMps: cfgSpeedCap\n };\n const row = await appendTelemetry(auth.uid, deviceId, {\n lat,\n lon,\n speed,\n battery: payload?.battery,\n mode: payload?.mode || (failsafe ? \"FAILSAFE_HOLD\" : \"ACTIVE\"),\n raw: payload?.raw || {}\n });\n await updateDevice(auth.uid, deviceId, {\n status: failsafe ? \"failsafe\" : \"active\",\n lastTelemetryAt: row.ts,\n safetyState: safety\n });\n if (failsafe) {\n await addTranscript(auth.uid, {\n source: \"robotics-guardrails\",\n eventType: \"failsafe\",\n text: `Device ${deviceId} failsafe triggered geofenceOk=${geofenceOk} speedOk=${speedOk}`,\n roomId: \"studio-main\",\n meta: { deviceId, safety }\n });\n }\n sendJson(res, 200, { ok: true, telemetry: row, safety, command: failsafe ? \"FAILSAFE_HOLD\" : \"CONTINUE\" });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"telemetry_ingest_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listTelemetry(auth.uid, 150);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createAutomation(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"automation-scheduled\",\n text: `Automation scheduled: ${row.title} at ${new Date(row.scheduleAt).toLocaleString()}`,\n roomId: \"studio-main\",\n meta: { automationId: row.id, action: row.action, deviceId: row.deviceId }\n });\n sendJson(res, 200, { ok: true, automation: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"automation_create_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAutomations(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listDevices(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const devs = await listDevices(auth.uid, 500);\n const d = devs.find((x) => x.id === deviceId);\n if (!d) return sendJson(res, 404, { ok: false, error: \"device_not_found\" });\n const action = String(payload?.action || \"\");\n const caps = Array.isArray(d.capabilityProfile) ? d.capabilityProfile : [];\n if (action && caps.length && !caps.includes(action) && !caps.includes(\"*\")) {\n return sendJson(res, 400, { ok: false, error: \"unsupported_action_for_device\", capabilityProfile: caps });\n }\n const cmd = await enqueueCommand(auth.uid, {\n deviceId,\n action: action || \"NOOP\",\n params: payload?.params || {},\n adapter: payload?.adapter || d.adapter || \"websocket\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-command-queue\",\n eventType: \"command-queued\",\n text: `${cmd.deviceId} -> ${cmd.action}`,\n roomId: \"studio-main\",\n meta: { commandId: cmd.id, adapter: cmd.adapter }\n });\n sendJson(res, 200, { ok: true, command: cmd });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listCommands(auth.uid, 200);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/robotics/commands/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updateCommandStatus(auth.uid, id, payload?.status || \"sent\");\n sendJson(res, 200, { ok: true, command: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/rhizoh/bridge\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const result = await runRhizohBrainV2({\n message: String(payload?.message || \"robotics bridge command\"),\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n worldState: payload?.worldState || {},\n deviceState: payload?.deviceState || {},\n intent: \"robotics-mechanics\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-bridge\",\n eventType: \"robotics-command\",\n text: `Bridge -> ${String(result.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { directive: result.directive, agentId: payload?.agentId || \"\" }\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"robotics_bridge_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/planner/graph\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const graph = buildTaskActionGraph(String(payload?.task || \"\"), payload?.context || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"planner-graph\",\n text: `Planner graph built: ${graph.taskTitle} nodes=${graph.nodes.length}`,\n roomId: \"studio-main\",\n meta: { taskTitle: graph.taskTitle, nodeCount: graph.nodes.length }\n });\n sendJson(res, 200, { ok: true, graph });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"planner_graph_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerSocialChannel(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, channel: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_channel_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listSocialChannels(auth.uid, 100);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/broadcast\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const text = String(payload?.text || \"\").slice(0, 1200);\n const channels = await listSocialChannels(auth.uid, 100);\n const enabled = channels.filter((c) => c.status === \"enabled\");\n const deliveries = [];\n for (const c of enabled) {\n const out = await deliverSocialWithRetry(c, text);\n deliveries.push({ id: c.id, platform: c.platform, ...out });\n }\n const okCount = deliveries.filter((d) => d.ok).length;\n await addTranscript(auth.uid, {\n source: \"social-layer\",\n eventType: \"social-broadcast\",\n text: `Broadcast delivered ${okCount}/${enabled.length} channel(s): ${text.slice(0, 240)}`,\n roomId: \"studio-main\",\n meta: { deliveries }\n });\n sendJson(res, 200, { ok: true, sentChannels: okCount, totalChannels: enabled.length, deliveries });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_broadcast_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/ethics/programs\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, ...buildEthicsPrograms() });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands/dispatch\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const all = await listCommands(auth.uid, 200);\n const queued = all.filter((c) => String(c.status || \"queued\") === \"queued\").slice(0, 50);\n const devices = await listDevices(auth.uid, 500);\n const byId = new Map(devices.map((d) => [d.id, d]));\n const results = [];\n for (const cmd of queued) {\n const dev = byId.get(cmd.deviceId);\n if (!dev) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: \"device_not_found\" });\n continue;\n }\n try {\n const dispatched = await dispatchCommandToAdapter(dev, cmd);\n await updateCommandStatus(auth.uid, cmd.id, \"sent\");\n results.push({ id: cmd.id, ok: true, adapter: dispatched.adapter, via: dispatched.via });\n } catch (error) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: String(error?.message || \"dispatch_failed\") });\n }\n }\n await addTranscript(auth.uid, {\n source: \"robotics-command-dispatch\",\n eventType: \"command-dispatch-batch\",\n text: `Dispatch batch size=${results.length} ok=${results.filter((x) => x.ok).length}`,\n roomId: \"studio-main\",\n meta: { results: results.slice(0, 30) }\n });\n sendJson(res, 200, { ok: true, processed: results.length, results });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"dispatch_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/pdf/jobs/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n const job = await getPdfJob(auth.uid, id);\n if (!job) return sendJson(res, 404, { ok: false, error: \"pdf_job_not_found\" });\n sendJson(res, 200, { ok: true, job });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const row = await appendMemory({\n scope,\n id,\n text: payload?.text,\n tags: payload?.tags || [],\n importance: payload?.importance,\n kind: payload?.kind || \"episodic\",\n meta: { uid: auth.uid, ...(payload?.meta || {}) }\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.externalTruth) {\n try {\n if (process.env.RHIZOH_EXTERNAL_TRUTH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_truth:${ip}`, RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n sendJson(res, 200, buildRhizohExternalGroundTruthPayload(req));\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.productOutcome) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const rawBody = await readHttpBodyText(req, 64 * 1024);\n const body = rawBody.trim() ? safeJsonParse(rawBody) : null;\n if (!body || typeof body !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"invalid_json_payload\" });\n return;\n }\n const sigHdr = String(req.headers?.[\"x-rhizoh-outcome-signature\"] || \"\").trim();\n const hmacV = verifyRhizohOutcomeHmac(rawBody, sigHdr);\n if (!hmacV.ok) {\n sendJson(res, 401, { ok: false, error: hmacV.error || \"outcome_hmac_invalid\" });\n return;\n }\n const srcTokHdr = String(req.headers?.[\"x-rhizoh-outcome-source-token\"] || \"\").trim();\n const srcV = verifyRhizohOutcomeSourceToken(body.source, srcTokHdr);\n if (!srcV.ok) {\n sendJson(res, 401, { ok: false, error: srcV.error || \"outcome_source_token_invalid\" });\n return;\n }\n const subjectKey = String(body.subjectKey || \"\").trim().slice(0, 128);\n if (\n subjectKey &&\n !checkHttpRateLimit(\n `rhizoh_product_outcome_subj:${subjectKey}`,\n RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN,\n 60_000\n )\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_subject\" });\n return;\n }\n const result = ingestRhizohProductOutcomeHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n schemaVersion: \"1.1.0\",\n aggregate: result.aggregate,\n integrity: result.integrity\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"outcome_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.productOutcomeAggregate) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome_agg:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const u = new URL(String(req.url || \"/\"), \"http://localhost\");\n const cohortId = u.searchParams.get(\"cohortId\") || \"\";\n const decisionFingerprint = u.searchParams.get(\"decisionFingerprint\") || \"\";\n const limit = u.searchParams.get(\"limit\") || \"50\";\n const rows = listRhizohProductOutcomeAggregates({\n cohortId: cohortId || undefined,\n decisionFingerprint: decisionFingerprint || undefined,\n limit: Number(limit) || 50\n });\n sendJson(res, 200, { ok: true, schemaVersion: \"1.1.0\", rows });\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.externalLossBatch) {\n try {\n if (process.env.RHIZOH_EXTERNAL_LOSS_BATCH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_loss_batch:${ip}`, RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const result = ingestRhizohExternalLossBatchHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, { ok: true, accepted: result.accepted ?? 0 });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"batch_failed\") });\n }\n return;\n }\n\n function moderationAdminOk(req) {\n const expected = String(process.env.CASTLE_MODERATION_ADMIN_KEY || \"\").trim();\n if (!expected) return false;\n const got = String(req.headers[\"x-castle-moderation-key\"] || \"\").trim();\n return got.length > 0 && got === expected;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.opsAbuseReport) {\n try {\n const body = await readHttpJson(req, 32 * 1024);\n const auth = await resolveHttpUser(req);\n const entry = submitAbuseReportV0({\n reporterUid: auth.ok ? auth.uid : null,\n targetUid: body?.targetUid ?? null,\n category: body?.category,\n detail: body?.detail,\n traceId: body?.traceId ?? null\n });\n sendJson(res, 202, { ok: true, reportId: entry.id, status: entry.status });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsModerationQueue) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n queue: listModerationQueueV0(100),\n softBlocked: listSoftBlockedUsersV0()\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-block\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const out = softBlockUserV0(body?.uid, body?.reason);\n sendJson(res, out.ok ? 200 : 400, out);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-unblock\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n sendJson(res, 200, removeSoftBlockV0(body?.uid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsAgentSnapshots) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n const limit = Number(new URL(req.url || \"\", \"http://local\").searchParams.get(\"limit\") || 50);\n sendJson(res, 200, { ok: true, snapshots: listRecentAgentSnapshotsV0(limit) });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.academicObservatoryExport) {\n const auth = await resolveHttpUser(req);\n const out = handleAcademicObservatoryExportGetV0(req, auth);\n sendJson(res, out.status, out.body);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsHardeningStatus) {\n const auth = await resolveHttpUser(req);\n const principal = auth.ok ? `uid:${auth.uid}` : null;\n const gclSnap = await getGlobalCostLedgerSnapshotV0(principal);\n const economicStrategy = evaluateEconomicStrategyL3V0(gclSnap);\n const hardeningTenantId = resolveHardeningTenantIdV0(auth, req);\n const unifiedState = await buildUnifiedStateNarrativeV0({\n tenantId: hardeningTenantId ?? undefined,\n platformScope: !hardeningTenantId,\n principal\n });\n sendJson(res, 200, {\n ok: true,\n schema: \"rhizoh.ops.hardening_status.v0\",\n containment: readAgentContainmentConfigV0(),\n cost: readCostContainmentConfigV0(),\n gcl: {\n backend: await getGclBackendStatusV0(),\n snapshot: gclSnap\n },\n economicStrategyL3: economicStrategy,\n phasedRollout: await getPhasedRolloutStatsV0(),\n tier: readPhasedRolloutTierV0(),\n principalCost: auth.ok ? await getCostContainmentStatsV0(principal) : null,\n stressTaxonomy: verifyStressTaxonomyCoverageV0(),\n stressConflictResolution: verifyStressConflictResolutionV0(),\n resolutionStabilityEnvelope: verifyResolutionStabilityEnvelopeV0(),\n unifiedState: {\n tenantScope: {\n tenantId: unifiedState.tenantScope?.tenantId,\n scope: unifiedState.tenantScope?.scope,\n noGlobalDerivedState: unifiedState.tenantScope?.noGlobalDerivedState\n },\n narrativeFingerprint: unifiedState.narrativeFingerprint,\n screenshotScopeWatermark: unifiedState.screenshotScopeWatermark,\n interpretationSafetyContract: unifiedState.interpretationSafetyContract,\n interpretationUxContract: {\n schema: unifiedState.interpretationUxContract?.schema,\n layerOrder: unifiedState.interpretationUxContract?.layerOrder,\n narrativeNeverVisuallyDominant: unifiedState.interpretationUxContract?.narrativeNeverVisuallyDominant\n },\n governance: {\n narrativeIsNotDecisionLayer: unifiedState.governance.narrativeIsNotDecisionLayer,\n displayOrder: unifiedState.governance.displayOrder,\n trustPosture: unifiedState.governance.trustPosture,\n interpretationUxContract: unifiedState.governance.interpretationUxContract?.schema\n },\n humanOps: {\n decisionTier: unifiedState.humanOps?.humanDecisionScaling?.tier,\n decisionOwner: unifiedState.humanOps?.humanDecisionScaling?.decisionOwner,\n opsRouteId: unifiedState.humanOps?.humanDecisionOpsRunbook?.routing?.routeId,\n slaAckMinutes: unifiedState.humanOps?.humanDecisionOpsRunbook?.sla?.ackMinutes,\n uiDisclaimerRequired: unifiedState.humanOps?.narrativeUiSafety?.displayRules?.requireDisclaimerBanner,\n misreadHighResidual: unifiedState.humanOps?.misreadSimulation?.highResidualCount,\n propagationHighResidual: unifiedState.humanOps?.socialPropagationSimulation?.highResidualCount,\n worstWatermarkSurvivability:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.worstWatermarkSurvivability,\n dominantDistortionSource:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.dominantDistortionSource\n },\n culturalRisk: {\n perceptualRiskClass: unifiedState.culturalRisk?.perceptualRiskClass,\n trustFork: unifiedState.culturalRisk?.trustDynamics?.fork,\n trustDecayScore: unifiedState.culturalRisk?.trustDynamics?.scores?.trustDecay,\n mythologyScore: unifiedState.culturalRisk?.trustDynamics?.scores?.mythology,\n confidenceDetachmentExposure:\n unifiedState.culturalRisk?.trustDynamics?.axes?.confidenceDetachmentExposure\n },\n appliedSystemsLayer: {\n schema: unifiedState.appliedSystemsLayer?.schema,\n researchOutputType: unifiedState.appliedSystemsLayer?.researchGovernance?.outputType,\n roboticsBlockActuation: unifiedState.appliedSystemsLayer?.roboticsGrounding?.blockActuation,\n spiralRumorCount:\n unifiedState.appliedSystemsLayer?.spiralMMOGameKernel?.gameLayer?.rumors?.length\n },\n actionSemanticGovernance: {\n schema: unifiedState.actionSemanticGovernance?.schema,\n highRiskActionCount: unifiedState.actionSemanticGovernance?.highRiskActionCount,\n loopClosed: unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.loopClosed,\n authorityCollapseMitigated:\n unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.risks?.authorityCollapse\n ?.mitigated\n },\n actionContextResolution: {\n schema: unifiedState.actionContextResolution?.schema,\n contextFingerprint: unifiedState.actionContextResolution?.contextFingerprint?.fingerprintHash,\n domainCollision: unifiedState.actionContextResolution?.domainCollision?.detected,\n deployAgentSpiralBlocked:\n unifiedState.actionContextResolution?.actionOverloadingRisk?.deployAgentExample\n ?.spiral_mmo === \"blocked\",\n roboticsActuationEligible:\n unifiedState.actionContextResolution?.roboticsActuationCorrectness?.actuationEligible\n },\n epistemicCoherence: {\n schema: unifiedState.epistemicCoherence?.schema,\n verdict: unifiedState.epistemicCoherence?.systemCoherence?.verdict,\n coherenceScore: unifiedState.epistemicCoherence?.systemCoherence?.score,\n contradictionCount: unifiedState.epistemicCoherence?.crossLayerContradictions?.count,\n operatorDecision: unifiedState.epistemicCoherence?.uxCompression?.operatorDecision,\n headlineTr: unifiedState.epistemicCoherence?.uxCompression?.headline?.tr,\n falseSafetyWarning: Boolean(\n unifiedState.epistemicCoherence?.uxCompression?.falseSafetyWarning\n )\n },\n coherenceAuthorityBoundary: {\n schema: unifiedState.coherenceAuthorityBoundary?.schema,\n bindingValid: unifiedState.coherenceAuthorityBoundary?.eclAuthorityBinding?.valid,\n canExecute: unifiedState.coherenceAuthorityBoundary?.contract?.can_execute,\n mandatoryBannerTr:\n unifiedState.coherenceAuthorityBoundary?.disclaimers?.mandatoryBanner?.tr\n },\n decisionLatencyGovernance: {\n schema: unifiedState.decisionLatencyGovernance?.schema,\n latencyTier: unifiedState.decisionLatencyGovernance?.latencyTier,\n inflationScore: unifiedState.decisionLatencyGovernance?.latencyInflation?.score,\n fastPathEligible: unifiedState.decisionLatencyGovernance?.fastPath?.eligible,\n primaryActionId: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.primaryActionId,\n ackSlaMinutes: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.ackSlaMinutes,\n certaintyCap: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.certaintyCap,\n uncertaintyItems:\n unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.uncertaintyEnvelope?.itemCount\n },\n decisionPacketUncertaintyBoundary: {\n schema: unifiedState.decisionPacketUncertaintyBoundary?.schema,\n invariantValid: unifiedState.decisionPacketUncertaintyBoundary?.invariantCheck?.valid,\n centralInvariant: unifiedState.decisionPacketUncertaintyBoundary?.centralInvariant\n },\n epistemicDecisionPacing: {\n schema: unifiedState.epistemicDecisionPacing?.schema,\n queueSaturationTier:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation?.tier,\n operatorProcessingLatency:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation\n ?.operatorProcessingLatency,\n postureWindowCount:\n unifiedState.epistemicDecisionPacing?.temporaryStaticPostureWindows\n ?.temporaryStaticPostureWindows?.windowCount,\n executionBoundariesValid:\n unifiedState.epistemicDecisionPacing?.executionBoundaries?.valid,\n grantsExecutionApproval: unifiedState.epistemicDecisionPacing?.grantsExecutionApproval\n },\n epistemicTemporalCoherence: {\n schema: unifiedState.epistemicTemporalCoherence?.schema,\n allInvariantsValid: unifiedState.epistemicTemporalCoherence?.allInvariantsValid,\n temporalAlignmentValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.temporalAlignment?.valid,\n crossWindowGuardValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard?.valid,\n delayedTruthValid: unifiedState.epistemicTemporalCoherence?.invariants?.delayedTruthRule?.valid,\n reconciliationRequired:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard\n ?.reconciliation?.required\n },\n realityDriftObserver: {\n schema: unifiedState.realityDriftObserver?.schema,\n driftActive: unifiedState.realityDriftObserver?.driftObserverSummary?.active,\n misapprehensionShapeCount:\n unifiedState.realityDriftObserver?.misapprehensionShapeCatalog?.shapeCount,\n criticalShapeCount:\n unifiedState.realityDriftObserver?.driftObserverSummary?.criticalShapeCount,\n roboticsMismatchActive:\n unifiedState.realityDriftObserver?.roboticsFeedbackMismatch?.active\n },\n driftCausality: {\n schema: unifiedState.driftCausality?.schema,\n shapesWithCausalExplanation: unifiedState.driftCausality?.rdolLinkage?.shapesWithCausalExplanation,\n activeDomainCount: unifiedState.driftCausality?.causalitySummary?.activeDomainCount,\n propagationCausalityActive: unifiedState.driftCausality?.causalDomains?.propagation?.active\n },\n productReadiness: unifiedState.productReadiness,\n stateLayers: {\n raw: unifiedState.stateLayers.raw,\n derived: {\n systemState: unifiedState.stateLayers.derived.systemState,\n narrative: unifiedState.stateLayers.derived.narrative,\n validation: unifiedState.stateLayers.derived.validation\n },\n policy: {\n binding: unifiedState.stateLayers.policy.binding,\n decisionOwner: unifiedState.stateLayers.policy.decisionOwner,\n prohibitedActions: unifiedState.stateLayers.policy.prohibitedActions\n }\n },\n systemState: unifiedState.systemState,\n interpretation: unifiedState.interpretation,\n validation: {\n trustPosture: unifiedState.validation.trustPosture,\n adjustedConfidence: unifiedState.validation.adjustedConfidence,\n agreement: unifiedState.validation.confidenceDecomposition.agreement,\n divergenceScore: unifiedState.validation.divergence.divergenceScore,\n validated: unifiedState.validation.divergence.validated,\n uncertaintyTags: unifiedState.validation.uncertainty.tags.map((t) => t.id),\n flags: unifiedState.validation.divergence.flags.map((f) => f.id)\n },\n compression: unifiedState.compression\n }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 6 * 1024 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (\n !checkHttpRateLimit(`rhizoh_voice_transcribe:${rlKey}`, RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN, 60_000)\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const voiceEnv = rhizohVoiceTranscribeEnvV3();\n if (!voiceEnv.any) {\n sendJson(res, 503, {\n ok: false,\n error: \"voice_asr_not_configured\",\n env: voiceEnv\n });\n return;\n }\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n const result = await runRhizohVoiceTranscribeV3(\n applyLanguagePropagationToVoiceBodyV1(payload, langProp)\n );\n sendJson(res, result.ok ? 200 : 422, {\n ...result,\n traceId: String(payload?.traceId || \"\"),\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n atMs: Date.now()\n });\n } catch (error) {\n sendJson(res, 500, {\n ok: false,\n error: \"voice_transcribe_failed\",\n detail: String(error?.message || error),\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(\"/rhizoh/llm/task/\")) {\n try {\n const rawTaskId = decodeURIComponent(pathname.slice(\"/rhizoh/llm/task/\".length).split(\"/\")[0] || \"\");\n const taskId = String(rawTaskId || \"\").toLowerCase() === \"last\" ? getLatestLlmWorkerTaskIdV0() : rawTaskId;\n const snap = taskId ? getLlmWorkerTaskSnapshotV0(taskId) : null;\n if (!snap) {\n sendJson(res, 404, {\n ok: false,\n error: \"llm_task_not_found\",\n taskId: rawTaskId,\n status: \"missing\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n if (snap.status === \"processing\") {\n sendJson(res, 202, {\n ok: true,\n taskId: snap.taskId,\n status: \"processing\",\n createdAt: snap.createdAt,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n // Terminal states (completed/failed) always HTTP 200 — poll must not look like server fault (500).\n sendJson(res, 200, {\n ...(snap.body || { ok: false, error: \"llm_task_empty\", taskId: snap.taskId }),\n taskId: snap.taskId,\n status: snap.status === \"completed\" ? \"completed\" : snap.status,\n terminal: true,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n } catch (error) {\n console.error(\"[llm-task-poll] handler failed:\", error);\n sendJson(res, 200, {\n ok: false,\n error: \"llm_task_poll_handler_failed\",\n detail: String(error?.message || error),\n status: \"failed\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === rhizohRuntime.routes.rhizohLlm) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 128 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n logRhizohHealth(\"gateway_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: String(payload?.traceId || \"\"),\n auth: auth.ok ? \"ok\" : \"anon\",\n ip\n });\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (!checkHttpRateLimit(`rhizoh_llm:${rlKey}`, RL_RHIZOH_LLM_PER_MIN, 60_000)) {\n const rlTaxonomy = classifyStressResponseV0({ code: \"rate_limit_exceeded\" });\n return sendJson(res, 429, {\n ok: false,\n error: \"rate_limit_exceeded\",\n reply: rlTaxonomy.userMessageTr || \"İstek sınırı aşıldı. Kısa süre sonra tekrar deneyin.\",\n stressClass: rlTaxonomy.stressClass,\n responseAction: rlTaxonomy.responseAction,\n stressMatrix: rlTaxonomy.matrix,\n stressInterpretable: rlTaxonomy.interpretable,\n stressConfidence: rlTaxonomy.stressConfidence,\n actionConfidence: rlTaxonomy.actionConfidence,\n actionSoftened: rlTaxonomy.actionSoftened,\n responseActionStrict: rlTaxonomy.responseActionStrict,\n actionInterpretable: rlTaxonomy.actionInterpretable,\n conflictResolution: rlTaxonomy.conflictResolution,\n directive: \"NONE\"\n });\n }\n\n const requireExplicit = process.env.CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE === \"1\";\n const keyMode = normalizeClientLlmKeySource(payload?.llmKeySource ?? payload?.keySource);\n if (keyMode === null) {\n return sendJson(res, 400, {\n ok: false,\n error: \"invalid_llm_key_source\",\n reply: \"llmKeySource geçersiz. Kullanın: env | user_connection | auto\",\n directive: \"NONE\"\n });\n }\n if (requireExplicit && keyMode === \"auto\") {\n return sendJson(res, 400, {\n ok: false,\n error: \"llm_key_source_required\",\n reply: \"Sunucu CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE=1: istekte llmKeySource (env veya user_connection) zorunlu.\",\n directive: \"NONE\"\n });\n }\n\n const conn = auth.ok ? await resolveConnection(auth.uid, String(payload?.connectionId || \"\")) : null;\n\n if (keyMode === \"user_connection\") {\n if (!auth.ok) {\n return sendJson(res, 401, {\n ok: false,\n error: \"llm_auth_required_for_user_connection\",\n reply: \"Kayıtlı LLM bağlantısı için giriş gerekli.\",\n directive: \"NONE\"\n });\n }\n if (!conn?.apiKey) {\n return sendJson(res, 400, {\n ok: false,\n error: \"user_llm_connection_required\",\n reply: \"Geçerli bir LLM bağlantısı yok; Studio’da bağlantı ekleyin veya llmKeySource=env kullanın.\",\n directive: \"NONE\"\n });\n }\n }\n\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n let safePayload = applyLanguagePropagationToLlmPayloadV1({ ...(payload || {}) }, langProp);\n const trimOut = trimRhizohLlmGatewayPayloadV1(safePayload);\n if (trimOut.meta.trimmed) {\n logRhizohHealth(\"llm_payload_trimmed\", {\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n bytesBefore: trimOut.meta.bytesBefore,\n bytesAfter: trimOut.meta.bytesAfter\n });\n }\n safePayload = trimOut.payload;\n delete safePayload.apiKey;\n delete safePayload.llmKeySource;\n delete safePayload.keySource;\n\n const llmMessage = String(\n safePayload?.message || safePayload?.text || payload?.message || payload?.text || \"\"\n ).trim();\n if (!llmMessage) {\n return sendJson(res, 400, {\n ok: false,\n error: \"message_required\",\n reply: \"Mesaj metni gerekli (message alanı boş).\",\n directive: \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n if (!safePayload.message) {\n safePayload = { ...safePayload, message: llmMessage };\n }\n\n let resolvedProvider = payload?.provider;\n let resolvedModel = payload?.model;\n let connApiKey = \"\";\n\n ({\n resolvedProvider,\n resolvedModel,\n connApiKey\n } = resolveServerLlmProviderV0(keyMode, conn, payload));\n\n const turnInput = {\n safePayload,\n auth,\n keyMode,\n conn,\n resolvedProvider,\n resolvedModel,\n connApiKey\n };\n const turnMeta = {\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n langEcho: buildRhizohLanguagePropagationEchoV1(langProp)\n };\n\n if (\n isLlmWorkerEnabledV0() &&\n isLlmWorkerAsyncEnabledV0() &&\n String(req.headers[\"x-rhizoh-llm-sync\"] || \"\").trim() !== \"1\"\n ) {\n const taskId = enqueueLlmWorkerTaskAsyncV0(turnInput, turnMeta);\n logRhizohHealth(\"llm_worker_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n taskId,\n traceId: String(payload?.traceId || taskId),\n keyMode,\n async: true\n });\n sendJson(res, 202, {\n ok: true,\n taskId,\n status: \"processing\",\n traceId: String(payload?.traceId || taskId),\n pollPath: `/rhizoh/llm/task/${taskId}`,\n gatewayPid: process.pid,\n ...turnMeta.langEcho\n });\n return;\n }\n\n const { result, traceId, turnLatencyMs, spinePhases, sampledTrace } = isLlmWorkerEnabledV0()\n ? await runLlmWorkerTaskV0(turnInput, turnMeta)\n : await rhizohGatewayTurn(turnInput);\n logRhizohHealth(\"llm_response\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId,\n turnLatencyMs: Number(turnLatencyMs || 0),\n keyMode\n });\n sendJson(res, 200, {\n ...result,\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n traceId,\n turnLatencyMs,\n ...(spinePhases ? { spinePhases } : {}),\n ...(sampledTrace !== undefined ? { sampledTrace } : {})\n });\n } catch (error) {\n const msg = String(error?.message || \"\");\n const code = error?.code || \"\";\n let status = 500;\n if (msg === \"payload_too_large\") status = 413;\n else if (msg === \"rate_limit_exceeded\") status = 429;\n else if (\n code === \"cost_hard_limit\" ||\n code === \"phased_rollout_capacity\" ||\n error?.containment === true ||\n String(code).startsWith(\"agent_\")\n ) {\n status = 429;\n } else if (code === \"user_soft_blocked\" || code === \"prompt_abuse_detected\") status = 403;\n else if (\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(code) ||\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(msg) ||\n msg.includes(\"missing_api_key_for_\")\n )\n status = 400;\n let rhizohFailureKind = \"provider_error\";\n if (msg === \"rate_limit_exceeded\" || status === 429) rhizohFailureKind = \"rate_limit\";\n else if (status === 403) rhizohFailureKind = \"policy_block\";\n else if (status === 400 || status === 413) rhizohFailureKind = \"client_config\";\n else if (msg.startsWith(\"provider_http_\")) {\n const st = Number(msg.replace(\"provider_http_\", \"\"));\n if (st === 408 || st === 504) rhizohFailureKind = \"timeout\";\n else rhizohFailureKind = \"provider_error\";\n } else if (/timeout|timed out|ETIMEDOUT/i.test(msg)) rhizohFailureKind = \"timeout\";\n\n const providerHttpMatch = msg.match(/^provider_http_(\\d{3})$/);\n const stressTaxonomy =\n error?.stressClass != null\n ? {\n stressClass: error.stressClass,\n responseAction: error.responseAction,\n stressMatrix: error.stressMatrix,\n stressInterpretable: error.stressInterpretable !== false,\n stressConfidence: error.stressConfidence,\n actionConfidence: error.actionConfidence,\n actionSoftened: error.actionSoftened,\n responseActionStrict: error.responseActionStrict,\n actionInterpretable: error.actionInterpretable,\n conflictResolution: error.conflictResolution,\n stressSecondary: error.stressSecondary,\n responseActions: error.responseActions\n }\n : (() => {\n const t = classifyStressResponseV0({\n code: code || msg,\n rhizohFailureKind,\n providerHttpStatus: providerHttpMatch\n ? Number(providerHttpMatch[1])\n : null\n });\n return {\n stressClass: t.stressClass,\n responseAction: t.responseAction,\n stressMatrix: t.matrix,\n stressInterpretable: t.interpretable,\n stressConfidence: t.stressConfidence,\n actionConfidence: t.actionConfidence,\n actionSoftened: t.actionSoftened,\n responseActionStrict: t.responseActionStrict,\n actionInterpretable: t.actionInterpretable,\n conflictResolution: t.conflictResolution,\n stressSecondary: t.stressSecondary,\n responseActions: t.responseActions\n };\n })();\n sendJson(res, status, {\n ok: false,\n rhizohFailureKind,\n ...(providerHttpMatch ? { providerHttpStatus: Number(providerHttpMatch[1]) } : {}),\n error: code || msg || \"rhizoh_llm_failed\",\n detail: msg || code || undefined,\n reply:\n error?.reply ||\n (status === 500 ? \"Rhizoh bağlantısı geçici olarak kesildi.\" : msg),\n directive: error?.directive || \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n ...stressTaxonomy\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicSeal) {\n try {\n if (!EPISTEMIC_SEAL_SECRET) {\n sendJson(res, 503, { ok: false, error: \"epistemic_seal_disabled\" });\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`epistemic_seal:${ip}`, RL_EPISTEMIC_SEAL_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const truth = body.truth_contract;\n if (!truth || typeof truth !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"truth_contract_required\" });\n return;\n }\n const sealBody = {\n truth_contract: truth,\n runtime_hash: String(body.runtime_hash || \"\"),\n model_route: {\n provider: body.model_route?.provider ?? null,\n model: body.model_route?.model ?? null\n },\n memory_digest: String(body.memory_digest || \"\"),\n world_snapshot_hash: String(body.world_snapshot_hash || \"\"),\n timestamp: Number(body.timestamp) || Date.now()\n };\n const canonical = canonicalEpistemicSealString(sealBody);\n const { hash, signature } = hashAndSignEpistemicSeal(canonical, EPISTEMIC_SEAL_SECRET);\n recordGenesisEpistemicSealIssued(hash);\n const attestation = {\n schema: EPISTEMIC_SEAL_SCHEMA,\n algorithm: \"SHA-256+HMAC-SHA256\",\n keyHint: process.env.CASTLE_EPISTEMIC_SEAL_SECRET ? \"CASTLE_EPISTEMIC_SEAL_SECRET\" : \"CASTLE_GATEWAY_TOKEN\",\n issuedAt: Date.now(),\n gateway: { service: \"castle-gateway\", listenPort: PORT }\n };\n sendJson(res, 200, {\n ok: true,\n hash,\n signature,\n attestation,\n canonicalBytes: Buffer.byteLength(canonical, \"utf8\")\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"seal_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicLogsBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`epistemic_logs:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 120) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const persisted = await persistEpistemicLedgerBatch(auth.uid, entries);\n recordGenesisEpistemicLedgerPersisted(persisted.persisted);\n // Internal cognition layer: forecast stays gateway/ledger-only (no client exposure).\n try {\n await persistEpistemicForecastBatch(auth.uid, persisted.normalized || []);\n } catch {\n /* forecast failures must not break primary ledger ingest */\n }\n sendJson(res, 200, {\n ok: true,\n persisted: persisted.persisted,\n mode: persisted.mode,\n latest: persisted.normalized?.[persisted.normalized.length - 1] || null\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"epistemic_log_ingest_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.authorityLedgerBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`authority_ledger:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 80) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const witnessed = persistAuthorityLedgerWitnessBatchV1(auth.uid, entries, EPISTEMIC_SEAL_SECRET);\n if (witnessed.witnessed > 0) {\n recordGenesisEpistemicLedgerPersisted(witnessed.witnessed);\n const clientSeal = witnessed.lastWitness?.clientSealHash;\n if (clientSeal) recordGenesisEpistemicSealIssued(clientSeal);\n }\n sendJson(res, 200, {\n ok: true,\n witnessed: witnessed.witnessed,\n quarantined: witnessed.quarantined,\n chainHeight: witnessed.chainHeight,\n chainHead: witnessed.chainHead,\n lastWitness: witnessed.lastWitness,\n results: witnessed.results\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"authority_ledger_witness_failed\") });\n }\n return;\n }\n\n sendJson(res, 404, { ok: false, error: \"not_found\" });\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\nspiralWssForGenesis = wss;\nconst orchestrator = createOrchestrator();\nconst clientStats = new Map();\nlet broadcasterClientId = null;\nconst spiralState = {\n rooms: [{ id: \"room-default\", name: \"Spiral Default Arena\" }],\n activeByClient: new Map(),\n characters: new Map() // id -> {id,name,role,roomId,source}\n};\n\n/** Castle social rooms — keyed by `castleRoomKey` from {@link WS_MESSAGE.CASTLE_SOCIAL_PULSE}. */\nconst castleSocialRoomState = new Map();\nconst CASTLE_SOCIAL_MEMBER_TTL_MS = 90_000;\nconst CASTLE_SOCIAL_MAX_PULSE_BYTES = 8192;\nconst RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES = 5 * 1024 * 1024;\nconst RHIZOH_VOICE_LIVE_MAX_CHUNKS = 96;\n\n/**\n * Voice Engine v3 live media lane.\n * Client streams MediaRecorder chunks over the existing gateway WS; STOP reuses the\n * gateway ASR runner so orchestration remains the single authority downstream.\n */\nconst rhizohVoiceLiveSessions = new WeakMap();\n\nfunction appendRhizohVoiceLiveEventV0(session, type, payloadRefSeed = \"\") {\n if (!session) return null;\n session.eventSeq = (Number(session.eventSeq) || 0) + 1;\n const event = createRhizohImmutableEventV0({\n type,\n sessionId: session.sessionId,\n traceId: session.traceId,\n actorSeed: session.sessionId || session.traceId,\n eventSeq: session.eventSeq,\n payloadRef: createRhizohPayloadRefV0(payloadRefSeed || `${session.sessionId}:${type}:${session.eventSeq}`)\n });\n session.events = [...(session.events || []), event].slice(-64);\n return event;\n}\n\nfunction sendRhizohVoiceLiveEnvelopeV0(socket, type, payload) {\n try {\n if (socket?.readyState === 1) socket.send(JSON.stringify(createEnvelope(type, payload)));\n } catch {\n /* noop */\n }\n}\n\nfunction decodeRhizohVoiceLiveChunkV0(payload = {}) {\n const b64 = String(payload.audioBase64 || \"\").trim();\n if (!b64) return { ok: false, error: \"audio_base64_required\" };\n try {\n const buf = Buffer.from(b64, \"base64\");\n if (!buf.length) return { ok: false, error: \"audio_decode_empty\" };\n return { ok: true, buffer: buf };\n } catch {\n return { ok: false, error: \"audio_base64_invalid\" };\n }\n}\n\nfunction handleRhizohVoiceLiveStartV0(socket, payload = {}) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const sessionId = String(p.sessionId || `live_${Date.now().toString(36)}`).slice(0, 96);\n rhizohVoiceLiveSessions.set(socket, {\n sessionId,\n traceId: String(p.traceId || \"\").slice(0, 128),\n mimeType: String(p.mimeType || \"audio/webm\").slice(0, 80),\n languageCode: String(p.languageCode || \"tr-TR\").slice(0, 32),\n path: String(p.path || \"both\").slice(0, 24),\n chunks: [],\n chunkIndexes: [],\n totalBytes: 0,\n nextChunkIndex: 1,\n eventSeq: 0,\n events: [],\n startedAtMs: Date.now()\n });\n appendRhizohVoiceLiveEventV0(rhizohVoiceLiveSessions.get(socket), \"VOICE_LIVE_SESSION_STARTED\", `${sessionId}:live_session`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_START, {\n ok: true,\n sessionId,\n schema: \"castle.rhizoh.voice_live.gateway_lane.v0\"\n });\n}\n\nfunction handleRhizohVoiceLiveChunkV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const sequence = validateRhizohVoiceLiveChunkPayloadV0(payload, {\n expectedChunkIndex: session.nextChunkIndex\n });\n if (!sequence.ok) {\n rhizohVoiceLiveSessions.delete(socket);\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:chunk_reject`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: sequence.error,\n expectedChunkIndex: sequence.expectedChunkIndex,\n chunkIndex: sequence.chunkIndex\n });\n return;\n }\n const decoded = decodeRhizohVoiceLiveChunkV0(payload);\n if (!decoded.ok) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: decoded.error\n });\n return;\n }\n if (\n session.chunks.length >= RHIZOH_VOICE_LIVE_MAX_CHUNKS ||\n session.totalBytes + decoded.buffer.length > RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES\n ) {\n rhizohVoiceLiveSessions.delete(socket);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_audio_too_large\"\n });\n return;\n }\n session.chunks.push(decoded.buffer);\n session.chunkIndexes.push(sequence.chunkIndex);\n session.totalBytes += decoded.buffer.length;\n session.nextChunkIndex += 1;\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_CHUNK_ACCEPTED\", `${session.sessionId}:chunk:${sequence.chunkIndex}`);\n}\n\nfunction handleRhizohVoiceLiveStopV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n rhizohVoiceLiveSessions.delete(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const lastChunkIndex = Number(payload?.lastChunkIndex);\n if (Number.isInteger(lastChunkIndex) && lastChunkIndex !== session.chunks.length) {\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:stop_mismatch`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_chunk_count_mismatch\",\n expectedChunkCount: lastChunkIndex,\n receivedChunkCount: session.chunks.length\n });\n return;\n }\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_STOP_RECEIVED\", `${session.sessionId}:stop:${session.chunks.length}`);\n const audio = Buffer.concat(session.chunks);\n if (!audio.length) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"audio_empty\"\n });\n return;\n }\n runRhizohVoiceTranscribeV3({\n path: String(payload?.path || session.path || \"both\"),\n audioBase64: audio.toString(\"base64\"),\n mimeType: String(payload?.mimeType || session.mimeType || \"audio/webm\"),\n languageCode: String(payload?.languageCode || session.languageCode || \"tr-TR\"),\n traceId: String(payload?.traceId || session.traceId || \"\"),\n sessionId: session.sessionId,\n transport: \"gateway_ws_live\"\n })\n .then((result) => {\n appendRhizohVoiceLiveEventV0(session, result?.ok ? \"VOICE_TRANSCRIBE_FINAL\" : \"VOICE_TRANSCRIBE_FAILED\", `${session.sessionId}:final`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_FINAL, {\n ...result,\n sessionId: session.sessionId,\n transportPath: \"gateway_ws_live\",\n bytes: audio.length,\n chunkCount: session.chunks.length,\n chunkSequence: Object.freeze({\n ok: true,\n firstChunkIndex: session.chunkIndexes[0] || null,\n lastChunkIndex: session.chunkIndexes[session.chunkIndexes.length - 1] || null,\n receivedChunkCount: session.chunks.length\n }),\n eventTrace: Object.freeze((session.events || []).slice(-16)),\n recordedMs: Math.max(0, Date.now() - session.startedAtMs)\n });\n })\n .catch((e) => {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_transcribe_failed\",\n detail: String(e?.message || e)\n });\n });\n}\n\nfunction removeCastleSocialClientFromAllRooms(clientId) {\n const cid = String(clientId || \"\");\n if (!cid) return;\n for (const room of castleSocialRoomState.values()) {\n room.members.delete(cid);\n }\n}\n\nfunction buildCastleSocialRoomPayload(roomKey) {\n const room = castleSocialRoomState.get(roomKey);\n if (!room) return null;\n const now = Date.now();\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const roster = [...room.members.values()].map((m) => ({\n userId: m.userId,\n lastMs: m.lastMs,\n ...m.sp\n }));\n const peerFeeds =\n room.walPeers && room.walPeers.size\n ? [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }))\n : [];\n return { castleRoomKey: roomKey, seq: room.seq, roster, peerFeeds, ts: now };\n}\n\nconst CASTLE_WAL_PEER_MAX_FEED_BYTES = 48 * 1024;\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {{ ok: boolean, code?: string, reason?: string }} gate\n */\nfunction rejectWalPeerIngressV0(socket, gate) {\n const code = gate.code || \"wal_peer_rejected\";\n recordWalPeerRejectV0(code);\n recordSubstrateOtelEventV0(\"wal_peer_reject\", { code, reason: String(gate.reason || \"\").slice(0, 120) });\n socket.send(\n JSON.stringify(\n createEnvelope(WS_MESSAGE.ERROR, {\n error: gate.reason || \"WAL peer ingress rejected.\",\n code\n })\n )\n );\n}\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {Record<string, unknown> | null | undefined} walPeerFeed\n * @returns {boolean}\n */\nfunction gateWalPeerIngressV0(socket, walPeerFeed) {\n const authGate = assertWalPeerSocketAuthorityV0(socket);\n if (!authGate.ok) {\n rejectWalPeerIngressV0(socket, authGate);\n return false;\n }\n const sigGate = validateWalPeerFeedSignatureV0(\n walPeerFeed && typeof walPeerFeed === \"object\" ? walPeerFeed : {}\n );\n if (!sigGate.ok) {\n rejectWalPeerIngressV0(socket, sigGate);\n return false;\n }\n return true;\n}\n\nfunction handleCastleWalPeerFeed(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_WAL_PEER_MAX_FEED_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_WAL_PEER_FEED too large.\" })));\n return;\n }\n const walPeerFeed = p.walPeerFeed && typeof p.walPeerFeed === \"object\" ? p.walPeerFeed : {};\n if (!gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const castleRoomKey = String(p.castleRoomKey || \"wal:convergence\").slice(0, 64);\n const castleId = String(p.castleId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId,\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: room.members.get(socket.clientId)?.sp ?? {}\n });\n const peerFeeds = [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }));\n const body = {\n castleRoomKey,\n seq: room.seq,\n peerFeeds,\n ts: now\n };\n const env = createEnvelope(WS_MESSAGE.CASTLE_WAL_PEER_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys(), ...room.walPeers.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction handleCastleSocialPulse(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_SOCIAL_MAX_PULSE_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_SOCIAL_PULSE too large.\" })));\n return;\n }\n const castleRoomKey = String(p.castleRoomKey || \"default\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n const sp = p.socialPulse && typeof p.socialPulse === \"object\" ? p.socialPulse : {};\n const walPeerFeed =\n p.walPeerFeed && typeof p.walPeerFeed === \"object\"\n ? p.walPeerFeed\n : sp.walPeerFeed && typeof sp.walPeerFeed === \"object\"\n ? sp.walPeerFeed\n : null;\n if (walPeerFeed && !gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const spNext = walPeerFeed ? { ...sp, walPeerFeed } : { ...sp };\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: spNext\n });\n if (walPeerFeed) {\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId: String(p.castleId || userId).slice(0, 64),\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n }\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const body = buildCastleSocialRoomPayload(castleRoomKey);\n if (!body) return;\n const env = createEnvelope(WS_MESSAGE.CASTLE_SOCIAL_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcast(msgObj) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcastState() {\n broadcast(\n createEnvelope(WS_MESSAGE.BROADCAST_STATE, {\n broadcasterClientId,\n viewers: [...wss.clients].filter((c) => c.readyState === 1).length\n })\n );\n}\n\nfunction currentSpiralStatePayload() {\n return {\n rooms: spiralState.rooms,\n characters: [...spiralState.characters.values()]\n };\n}\n\nfunction broadcastSpiralState() {\n broadcast(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload()));\n}\n\nfunction ensureRoom(roomId, fallbackName = null) {\n if (!roomId) return spiralState.rooms[0].id;\n const exists = spiralState.rooms.find((r) => r.id === roomId);\n if (exists) return exists.id;\n const created = { id: roomId, name: fallbackName || `Spiral-${roomId.slice(-4)}` };\n spiralState.rooms.push(created);\n return created.id;\n}\n\nfunction syncSpiralCharactersWithWorld(snapshot) {\n const agents = snapshot?.agents || [];\n for (const a of agents) {\n if (!spiralState.characters.has(a.id)) {\n spiralState.characters.set(a.id, {\n id: a.id,\n name: a.id,\n role: \"agent\",\n roomId: spiralState.rooms[0].id,\n source: \"world\"\n });\n }\n }\n}\n\nfunction broadcastExcept(msgObj, exceptSocket) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client !== exceptSocket && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction validateInputFrame(payload, socket) {\n const stats = clientStats.get(socket.clientId) || { lastFrame: 0, windowStart: Date.now(), msgCount: 0 };\n const now = Date.now();\n if (now - stats.windowStart > 5000) {\n stats.windowStart = now;\n stats.msgCount = 0;\n }\n stats.msgCount += 1;\n clientStats.set(socket.clientId, stats);\n if (stats.msgCount > 120) return \"Rate limit exceeded.\";\n\n if (!payload || !Number.isInteger(payload.frame)) return \"frame must be integer.\";\n if (payload.frame <= stats.lastFrame) return \"frame must be monotonic.\";\n const commands = Array.isArray(payload.commands) ? payload.commands : [];\n if (commands.length > 8) return \"Too many commands in one frame.\";\n for (const cmd of commands) {\n if (!cmd || typeof cmd.command !== \"string\") return \"Invalid command format.\";\n if (\n cmd.command !== \"SPAWN_AGENT\" &&\n cmd.command !== \"LIST_CASTLES\" &&\n cmd.command !== \"SPAWN_ENTITY\" &&\n cmd.command !== \"CREATE_CASTLE\"\n ) return \"Unsupported command.\";\n }\n stats.lastFrame = payload.frame;\n clientStats.set(socket.clientId, stats);\n return null;\n}\n\nfunction validateSignalPayload(payload) {\n if (!payload || typeof payload !== \"object\") return \"Invalid signaling payload.\";\n const signalType = payload.signalType;\n if (Object.values(CASTLE_NETWORK_SIGNAL_V0).includes(signalType)) {\n return null;\n }\n if (![\"OFFER\", \"ANSWER\", \"ICE\"].includes(signalType)) return \"Invalid signal type.\";\n if (typeof payload.to !== \"string\" || payload.to.length < 3) return \"Invalid signal destination.\";\n if (signalType !== \"ICE\" && JSON.stringify(payload.sdp || {}).length > 16 * 1024) return \"SDP too large.\";\n if (signalType === \"ICE\" && JSON.stringify(payload.candidate || {}).length > 6 * 1024) return \"ICE too large.\";\n return null;\n}\n\nfunction sanitizeRawMessage(raw) {\n if (!raw) return null;\n const text = raw.toString();\n if (Buffer.byteLength(text, \"utf8\") > MAX_MESSAGE_BYTES) return null;\n return text;\n}\n\nwss.on(\"connection\", async (socket, req) => {\n const origin = req?.headers?.origin || \"\";\n if (!httpCors.isWebSocketOriginAllowed(origin)) {\n socket.close(1008, \"Origin not allowed\");\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const path = req?.url || \"\";\n const query = new URL(`ws://localhost${path}`).searchParams;\n const token = query.get(\"token\");\n if (!token || token !== REQUIRED_GATEWAY_TOKEN) {\n socket.close(1008, \"Invalid gateway token\");\n return;\n }\n }\n if (REQUIRE_AUTH) {\n const result = await verifyClientToken(req);\n if (!result.ok) {\n const isLocal = origin.includes(\"localhost\") || origin.includes(\"127.0.0.1\") || !origin;\n if (!(ALLOW_DEV_ANON && isLocal)) {\n socket.close(1008, result.reason);\n return;\n }\n socket.auth = { ok: true, kind: \"dev-anon\", user: { uid: \"dev-anon\" } };\n } else {\n socket.auth = result;\n }\n }\n\n socket.clientId = `c-${Math.random().toString(36).slice(2, 8)}`;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.HELLO, { message: \"Castle Gateway online.\", clientId: socket.clientId })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.WORLD_SNAPSHOT, orchestrator.worldSnapshot())));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.BROADCAST_STATE, { broadcasterClientId, viewers: [...wss.clients].filter((c) => c.readyState === 1).length })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload())));\n broadcastExcept(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }), socket);\n\n socket.on(\"message\", (raw) => {\n const safeRaw = sanitizeRawMessage(raw);\n if (!safeRaw) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Message too large or invalid.\" })));\n return;\n }\n const parsed = safeJsonParse(safeRaw);\n if (!parsed?.type) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid message.\" })));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND_TEXT) {\n const canonical = parseTextToCommand(parsed.payload?.text);\n if (!canonical) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Unknown command text.\" })));\n return;\n }\n const result = orchestrator.applyCommand(canonical);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND) {\n const result = orchestrator.applyCommand(parsed.payload);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n }\n\n if (parsed.type === WS_MESSAGE.INPUT_FRAME) {\n const err = validateInputFrame(parsed.payload, socket);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const frame = parsed.payload.frame;\n const commands = Array.isArray(parsed.payload?.commands) ? parsed.payload.commands : [];\n orchestrator.queueInputFrame(socket.clientId, frame, commands);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, queued: true, frame })));\n }\n\n if (parsed.type === WS_MESSAGE.SIGNAL) {\n const payload = parsed.payload || {};\n if (Object.values(CASTLE_NETWORK_SIGNAL_V0).includes(payload.signalType)) {\n const netErr = validateCastleNetworkSignalPayloadV0(socket, payload);\n if (netErr) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: netErr })));\n return;\n }\n handleCastleNetworkSignalV0(socket, payload, wss);\n return;\n }\n const err = validateSignalPayload(payload);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const signalType = payload.signalType;\n const to = payload.to;\n for (const client of wss.clients) {\n if (client.clientId === to && client.readyState === 1) {\n client.send(JSON.stringify(createEnvelope(WS_MESSAGE.SIGNAL, { ...payload, from: socket.clientId })));\n break;\n }\n }\n }\n\n if (parsed.type === WS_MESSAGE.DRONE_TELEMETRY) {\n const payload = parsed.payload || {};\n const json = JSON.stringify(payload);\n if (Buffer.byteLength(json, \"utf8\") > 48 * 1024) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"DRONE_TELEMETRY payload too large.\" })));\n return;\n }\n broadcastExcept(createEnvelope(WS_MESSAGE.DRONE_TELEMETRY, { ...payload, relayFrom: socket.clientId }), socket);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_START) {\n handleRhizohVoiceLiveStartV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_CHUNK) {\n handleRhizohVoiceLiveChunkV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_STOP) {\n handleRhizohVoiceLiveStopV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_SOCIAL_PULSE) {\n handleCastleSocialPulse(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_WAL_PEER_FEED) {\n handleCastleWalPeerFeed(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.OPEN_DATA_QUERY) {\n queryOpenData(parsed.payload)\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: true, result })));\n })\n .catch((e) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: false, error: e.message })));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_QUERY) {\n runRhizohBrainV2({\n ...(parsed.payload || {}),\n uid: socket?.auth?.user?.uid || \"unknown\"\n })\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, result)));\n })\n .catch(async () => {\n const fallback = await runRhizohBrain(parsed.payload || {}).catch(() => ({\n ok: false,\n reply: \"Rhizoh temporary disturbance. Habitat remains stable.\"\n }));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, fallback)));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.BROADCAST_REGISTER) {\n const role = String(parsed.payload?.role || \"\");\n if (role === \"GENESIS_BROADCAST_AGENT\") {\n broadcasterClientId = socket.clientId;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, registered: role })));\n broadcastState();\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.STUDIO_CUE) {\n // Only registered broadcaster can push global cues.\n if (!broadcasterClientId || socket.clientId !== broadcasterClientId) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Only broadcaster agent can emit STUDIO_CUE.\" })));\n return;\n }\n const cue = parsed.payload || {};\n if (typeof cue !== \"object\") {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid STUDIO_CUE payload.\" })));\n return;\n }\n broadcast(createEnvelope(WS_MESSAGE.STUDIO_CUE, cue));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_JOIN_ROOM) {\n const requested = String(parsed.payload?.roomId || \"\").trim();\n const requestedName = String(parsed.payload?.roomName || \"\").trim();\n const roomId = ensureRoom(requested || `room-${Math.random().toString(36).slice(2, 7)}`, requestedName || null);\n spiralState.activeByClient.set(socket.clientId, roomId);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, roomId })));\n broadcastSpiralState();\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_CREATE_CHARACTER) {\n const name = String(parsed.payload?.name || \"\").trim().slice(0, 32) || `AI-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;\n const role = String(parsed.payload?.role || \"agent\").trim().slice(0, 24) || \"agent\";\n const roomId = ensureRoom(\n String(parsed.payload?.roomId || spiralState.activeByClient.get(socket.clientId) || spiralState.rooms[0].id),\n null\n );\n\n // Character creation always maps to an authoritative world agent.\n const result = orchestrator.applyCommand({\n command: COMMAND.SPAWN_AGENT,\n params: { targetCastleId: \"castle-01\" }\n });\n if (!result?.ok || !result?.data?.id) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Character creation failed.\" })));\n return;\n }\n const id = result.data.id;\n spiralState.characters.set(id, { id, name, role, roomId, source: \"spawned\" });\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, characterId: id, roomId })));\n broadcastSpiralState();\n return;\n }\n });\n\n socket.on(\"close\", () => {\n if (socket.clientId === broadcasterClientId) broadcasterClientId = null;\n clientStats.delete(socket.clientId);\n rhizohVoiceLiveSessions.delete(socket);\n spiralState.activeByClient.delete(socket.clientId);\n removeCastleSocialClientFromAllRooms(socket.clientId);\n removeCastleNetworkClientV0(socket.clientId, wss);\n broadcast(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }));\n broadcastState();\n });\n});\n\nsetInterval(() => {\n void (async () => {\n try {\n const { frame, snapshot, delta, packedDelta } = orchestrator.tick(1 / 20);\n syncSpiralCharactersWithWorld(snapshot);\n broadcast(createEnvelope(WS_MESSAGE.NETWORK_TICK, { frame }));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_TICK, snapshot));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA, delta));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA_PACKED, packedDelta));\n broadcastSpiralState();\n await runAcademyEventTick();\n const executed = await tickAutomations(Date.now());\n for (const a of executed) {\n await addTranscript(a.uid, {\n source: \"planner\",\n eventType: \"automation-executed\",\n text: `Automation executed: ${a.title} (${a.action})`,\n roomId: \"studio-main\",\n meta: { automationId: a.id, action: a.action, deviceId: a.deviceId || \"\" }\n });\n }\n } catch (e) {\n console.error(\"[GATEWAY] world tick / automations loop error:\", String(e?.message || e));\n }\n })();\n}, 50);\n\n(async () => {\n await rhizohRuntime.telemetry.initOpenTelemetry();\n if (String(process.env.CASTLE_GENESIS_DISK_PERSIST ?? \"\").trim() === \"\") {\n process.env.CASTLE_GENESIS_DISK_PERSIST = \"1\";\n }\n const genesisDiskBoot = await hydrateGenesisContinuityPersistenceBootV0();\n if (!genesisDiskBoot.ok) {\n console.warn(\"[GATEWAY] genesis disk hydrate failed; continuing with in-memory genesis continuity\");\n }\n httpServer.listen(PORT, () => {\n startGenesisCanonicalClock();\n installGenesisCheckpointSurfaceGetter(buildGenesisRuntimeSurfacePayloadLive);\n setGenesisContinuityAfterPublishHook((seq) => {\n noteGenesisCheckpointSeqCommitted(seq);\n noteGenesisContinuitySeqAcceptedV0(seq);\n });\n startGenesisContinuityInfraSampler(buildGenesisRuntimeSurfacePayloadLive, 2000);\n logProductionObservatorySurfaceGuardsV0();\n logGatewaySubstrateAuthorityGuardsV0();\n if (isLlmWorkerEnabledV0()) {\n try {\n const workerBoot = initLlmWorkerAtBootV0();\n console.log(\"[GATEWAY] llm worker boot:\", workerBoot);\n } catch (workerErr) {\n console.error(\"[GATEWAY] llm worker boot failed:\", workerErr);\n }\n }\n console.log(`[GATEWAY] ws/http://localhost:${PORT}`);\n console.log(\n `[GATEWAY] genesis continuity: GET ${rhizohRuntime.routes.genesisRuntime} | POST ${rhizohRuntime.routes.genesisIngress} | SSE ${rhizohRuntime.routes.genesisStream} | replay ${rhizohRuntime.routes.genesisReplay} | diff ${rhizohRuntime.routes.genesisReplayDiff} | equiv ${rhizohRuntime.routes.genesisReplayEquivalence} | analytics ${rhizohRuntime.routes.genesisReplayAnalytics} | evolution ${rhizohRuntime.routes.genesisReplayEvolution}`\n );\n if (String(process.env.CASTLE_GENESIS_OBSERVATORY_LIVE_V01 ?? \"\").trim() === \"1\") {\n console.log(\"GENESIS OBSERVATORY LIVE v0.1 — READ ONLY PHASE STARTED\");\n }\n });\n})().catch((e) => {\n console.error(\"[GATEWAY] fatal boot (before or during listen):\", e);\n process.exit(1);\n});\n"}, "67f6b0d468e5f6516ce48398dbb886ba41cb0ca4": {"t": 1785754133.8641753, "n": 172705, "h": "a6b377a3175c7231", "c": "import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport dotenv from \"dotenv\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconsole.log(\"🔥 [GENESIS_BOOT] entry file:\", __filename);\nconsole.log(\"🔥 [GENESIS_BOOT] node env:\", process.env.NODE_ENV);\nconsole.log(\"🔥 [GENESIS_BOOT] pid:\", process.pid);\nconsole.log(\"🧬 [GENESIS_BOOT] cwd:\", process.cwd());\nconsole.log(\"🧬 [GENESIS_BOOT] import.meta.url:\", import.meta.url);\ndotenv.config({ path: path.join(__dirname, \"..\", \".env\") });\ndotenv.config({ path: path.join(__dirname, \"..\", \".env.local\"), override: true });\n\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport { createServer } from \"node:http\";\nimport { createOrchestrator } from \"@castle/orchestrator\";\nimport { parseTextToCommand } from \"@castle/command-dsl\";\nimport {\n WS_MESSAGE,\n COMMAND,\n createEnvelope,\n createRhizohImmutableEventV0,\n createRhizohPayloadRefV0,\n safeJsonParse,\n validateRhizohVoiceLiveChunkPayloadV0\n} from \"@castle/protocol\";\nimport { queryOpenData } from \"./openData.js\";\nimport { verifyClientToken } from \"./auth.js\";\nimport { runRhizohBrain } from \"./rhizohBrain.js\";\nimport { queryRhizohLlm } from \"./rhizohLlmGateway.js\";\nimport { rhizohGatewayTurn } from \"./rhizohGatewayTurn.js\";\nimport {\n enqueueLlmWorkerTaskAsyncV0,\n getLatestLlmWorkerTaskIdV0,\n getLlmWorkerDebugSnapshotV0,\n getLlmWorkerTaskSnapshotV0,\n initLlmWorkerAtBootV0,\n isLlmWorkerAsyncEnabledV0,\n isLlmWorkerEnabledV0,\n runLlmWorkerTaskV0\n} from \"./llmWorkerRuntime.js\";\nimport {\n RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n runRhizohVoiceTranscribeV3,\n rhizohVoiceTranscribeEnvV3\n} from \"./rhizohVoiceTranscribeV3.js\";\nimport { createHttpCorsPolicy, normalizeHttpOrigin, parseAllowedOriginsFromEnv } from \"./httpCorsPolicyV1.js\";\nimport {\n applyLanguagePropagationToLlmPayloadV1,\n applyLanguagePropagationToVoiceBodyV1,\n buildRhizohLanguagePropagationEchoV1,\n parseRhizohLanguagePropagationV1\n} from \"./rhizohLanguagePropagationV1.js\";\nimport { trimRhizohLlmGatewayPayloadV1 } from \"./rhizohLlmPayloadTrimV1.js\";\nimport {\n meshAppendDelta,\n meshContinuityAggregate,\n meshJoin,\n meshLeave,\n meshReplayFromNodeId,\n meshReplayFromSeq,\n meshSnapshot,\n meshSubscribeSse\n} from \"./presenceRoomMesh.js\";\nimport { runRhizohBrainV2 } from \"./rhizohBrainV2.js\";\nimport {\n listConnections,\n createConnection,\n updateConnection,\n deleteConnection,\n setDefaultConnection,\n resolveConnection\n} from \"./llmConnectionsStore.js\";\nimport { getCloudSyncSnapshotV0, mergeCloudSyncSnapshotV0 } from \"./castleCloudSyncStore.js\";\nimport {\n CASTLE_NETWORK_SIGNAL_V0,\n handleCastleNetworkSignalV0,\n listCastleNetworkPresenceV0,\n removeCastleNetworkClientV0,\n validateCastleNetworkSignalPayloadV0\n} from \"./castleNetworkRelayV0.js\";\nimport { checkHttpRateLimit, getHttpClientIp } from \"./castleHttpRateLimit.js\";\nimport {\n submitAbuseReportV0,\n listModerationQueueV0,\n softBlockUserV0,\n removeSoftBlockV0,\n listSoftBlockedUsersV0\n} from \"./ops/moderationMvpV0.js\";\nimport { listRecentAgentSnapshotsV0 } from \"./ops/agentObservabilityV0.js\";\nimport { readAgentContainmentConfigV0 } from \"./ops/agentContainmentV0.js\";\nimport { readCostContainmentConfigV0, getCostContainmentStatsV0 } from \"./ops/costContainmentV0.js\";\nimport { getGlobalCostLedgerSnapshotV0, getGclBackendStatusV0 } from \"./ops/globalCostLedgerV0.js\";\nimport { evaluateEconomicStrategyL3V0 } from \"./ops/economicStrategyEngineL3V0.js\";\nimport { getPhasedRolloutStatsV0, readPhasedRolloutTierV0 } from \"./ops/phasedRolloutV0.js\";\nimport { buildUnifiedStateNarrativeV0 } from \"./ops/unifiedStateNarrativeV0.js\";\nimport { resolveHardeningTenantIdV0 } from \"./ops/narrativeTenantIsolationV0.js\";\nimport {\n classifyStressResponseV0,\n verifyStressTaxonomyCoverageV0,\n verifyStressConflictResolutionV0\n} from \"./ops/stressResponseTaxonomyV0.js\";\nimport { verifyResolutionStabilityEnvelopeV0 } from \"./ops/resolutionStabilityEnvelopeV0.js\";\nimport { logLlmAccess } from \"./castleLlmAudit.js\";\nimport { appendMemory, listMemories, getMemoryContext, getPersonaGoalMemory, setPersonaGoalMemory, autoCompactMemories } from \"./memoryStore.js\";\nimport { getFirebasePersistence } from \"./firebasePersistence.js\";\nimport { registerAgentIdentity, listAgentIdentities, getAgentIdentity, updateAgentIdentity } from \"./agentIdentityStore.js\";\nimport { queueAcademyEvent, listAcademyEvents, resolveAcademyEvent, runAcademyEventTick } from \"./academyEventEngine.js\";\nimport { enqueuePdfJob, getPdfJob } from \"./pdfPipeline.js\";\nimport {\n createPublishSession,\n listPublishSessions,\n updatePublishSession,\n addTranscript,\n listTranscripts,\n searchMetadata,\n registerDevice,\n listDevices,\n updateDevice,\n appendTelemetry,\n listTelemetry,\n createAutomation,\n listAutomations,\n tickAutomations,\n enqueueCommand,\n listCommands,\n updateCommandStatus,\n registerSocialChannel,\n listSocialChannels\n} from \"./studioOpsStore.js\";\nimport { metrics as infraMetrics } from \"./infra/metrics.js\";\nimport { renderRhizohPrometheusMetrics, rhizohEnterpriseMetrics } from \"./infra/rhizohEnterpriseMetrics.js\";\nimport { scoreHealth } from \"./infra/healthScore.js\";\nimport { getTraceById, listTracesBySession } from \"./infra/traceRegistry.js\";\nimport { canonicalEpistemicSealString, hashAndSignEpistemicSeal, EPISTEMIC_SEAL_SCHEMA } from \"./epistemicSeal.js\";\nimport { persistEpistemicLedgerBatch } from \"./epistemicLedgerStore.js\";\nimport { persistEpistemicForecastBatch } from \"./epistemicForecastStore.js\";\nimport { buildRhizohExternalGroundTruthPayload } from \"./rhizohExternalGroundTruthGateway.js\";\nimport { ingestRhizohExternalLossBatchHttp } from \"./rhizohExternalLossIngestGateway.js\";\nimport {\n ingestRhizohProductOutcomeHttp,\n listRhizohProductOutcomeAggregates,\n verifyRhizohOutcomeHmac,\n verifyRhizohOutcomeSourceToken\n} from \"./rhizohProductOutcomeIngestGateway.js\";\nimport { initRhizoh } from \"./rhizohProductionBootstrap.js\";\nimport { handleAcademicObservatoryExportGetV0 } from \"./rhizoh/academicObservatoryHttpV0.js\";\nimport { handleCouncilAnomalyReasoningPostV0 } from \"./council/rhizohEpistemicCouncilHttpV0.js\";\nimport {\n handleLiveSportsBundleGetV0,\n handleLiveNewsHeadlinesGetV0,\n handleLiveWorldFeedGetV0\n} from \"./rhizoh/liveWorldFeedHttpV0.js\";\nimport {\n buildGenesisRuntimeSurfacePayload,\n recordGenesisEpistemicLedgerPersisted,\n recordGenesisEpistemicSealIssued,\n startGenesisCanonicalClock\n} from \"./genesisRuntimeSurfaceV0.js\";\nimport { registerGenesisSseClient, getGenesisContinuitySeq, setGenesisContinuityAfterPublishHook } from \"./genesisContinuityStreamHubV0.js\";\nimport { computeGenesisReplayFingerprintV0 } from \"./genesisReplayFingerprintV0.js\";\nimport { buildGenesisGatewayCapabilitiesV0 } from \"./genesisGatewayCapabilitiesV0.js\";\nimport {\n installGenesisCheckpointSurfaceGetter,\n noteGenesisCheckpointSeqCommitted,\n getLatestGenesisSignedCheckpoint\n} from \"./genesisContinuityCheckpointV0.js\";\nimport { hydrateGenesisContinuityPersistenceBootV0 } from \"./genesisContinuityHydrateBootV0.js\";\nimport { startGenesisContinuityInfraSampler } from \"./genesisContinuityInfraSamplerV0.js\";\nimport {\n genesisCheckpointQueryBySeqV0,\n genesisCheckpointQueryRangeV0,\n genesisCheckpointQueryLineageV0\n} from \"./genesisCheckpointQueryV0.js\";\nimport {\n genesisContinuityEventArchiveEnabled,\n queryGenesisContinuityEventArchiveV0,\n GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION\n} from \"./genesisContinuityEventArchiveV0.js\";\nimport { resolveGenesisReplayRouterV1, GENESIS_REPLAY_ROUTER_SCHEMA } from \"./genesisReplayRouterV1.js\";\nimport {\n computeGenesisReplayTemporalDiffV1,\n GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA\n} from \"./genesisReplayTemporalDiffV1.js\";\nimport { compareGenesisReplayEquivalenceV1 } from \"./genesisReplayEquivalenceV1.js\";\nimport { computeGenesisReplayAnalyticsV1 } from \"./genesisReplayAnalyticsV1.js\";\nimport { computeGenesisReplayEvolutionV1 } from \"./genesisReplayEvolutionV1.js\";\nimport { handleStripeCheckoutCreate, handleStripeMembershipWebhook } from \"./stripeWebhookMembership.js\";\nimport { buildRealityHealthPayload } from \"./edgeRealityConsistencyV1.js\";\nimport {\n assertWalPeerSocketAuthorityV0,\n buildGatewaySubstrateAuthoritySnapshotV0,\n logGatewaySubstrateAuthorityGuardsV0,\n validateWalPeerFeedSignatureV0\n} from \"./gatewaySubstrateAuthorityV0.js\";\nimport {\n buildSubstrateOperationalSnapshotV0,\n recordWalPeerRejectV0,\n renderSubstratePrometheusMetricsV0\n} from \"./infra/substrateOperationalMetrics.js\";\nimport { ingestClientSubstrateHealthV0 } from \"./rhizohSubstrateHealthIngestV0.js\";\nimport { recordSubstrateOtelEventV0 } from \"./infra/opentelemetrySubstrateV0.js\";\nimport {\n GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n getGenesisWorldObservationIngressStatusV0,\n ingestGenesisWorldObservationV0\n} from \"./genesisContinuityClientIngressV0.js\";\nimport {\n getGenesisContinuitySeqAuditV0,\n noteGenesisContinuitySeqAcceptedV0\n} from \"./genesisContinuitySeqGapV0.js\";\n\n/** Render / Fly / Railway: `PORT` — yoksa `CASTLE_GATEWAY_PORT` — yoksa 8090. */\nconst PORT =\n Number(process.env.PORT) ||\n Number(process.env.CASTLE_GATEWAY_PORT) ||\n 8090;\n/** Spiral / world WebSocket server — set after `wss` init; used by genesis runtime surface. */\nlet spiralWssForGenesis = null;\nconst MAX_MESSAGE_BYTES = Number(process.env.CASTLE_MAX_MESSAGE_BYTES || 32 * 1024);\nconst REQUIRED_GATEWAY_TOKEN = process.env.CASTLE_GATEWAY_TOKEN || \"\";\nconst ALLOWED_ORIGINS = parseAllowedOriginsFromEnv().fromList;\nconst httpCors = createHttpCorsPolicy();\n\n/** @returns {string|null} `Access-Control-Allow-Origin`; null = browser CORS fail. */\nfunction accessControlAllowOriginValue(req) {\n return httpCors.accessControlAllowOriginValue(req);\n}\n\nfunction applyHttpCorsHeaders(req, res) {\n return httpCors.applyHttpCorsHeaders(req, res);\n}\nconst REQUIRE_AUTH = process.env.CASTLE_REQUIRE_AUTH === \"true\";\nconst ALLOW_DEV_ANON = process.env.CASTLE_ALLOW_DEV_ANON !== \"false\";\nconst ALLOW_DEV_HTTP_UID = process.env.CASTLE_ALLOW_DEV_HTTP_UID !== \"false\";\n/** Presence mesh: allow `X-Castle-Guest-Id` when Firebase auth absent (default on). Set `false` to require auth only. */\nconst ALLOW_MESH_GUEST = process.env.CASTLE_ALLOW_MESH_GUEST !== \"false\";\nconst SOCIAL_RETRY_MAX = Math.max(0, Math.min(5, Number(process.env.CASTLE_SOCIAL_RETRY_MAX || 2)));\nconst SOCIAL_RETRY_BASE_MS = Math.max(100, Number(process.env.CASTLE_SOCIAL_RETRY_BASE_MS || 700));\nconst TELEGRAM_BOT_TOKEN = process.env.CASTLE_TELEGRAM_BOT_TOKEN || \"\";\nconst WHATSAPP_TOKEN = process.env.CASTLE_WHATSAPP_TOKEN || \"\";\nconst WHATSAPP_PHONE_NUMBER_ID = process.env.CASTLE_WHATSAPP_PHONE_NUMBER_ID || \"\";\nconst RL_RHIZOH_LLM_PER_MIN = Math.max(5, Number(process.env.CASTLE_RL_RHIZOH_LLM_PER_MIN || 40));\nconst RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN || 30)\n);\nconst RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN || 120));\nconst RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN = Math.max(\n 10,\n Number(process.env.CASTLE_RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN || 60)\n);\nconst RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN = Math.max(8, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN || 48));\nconst RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN = Math.max(4, Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN || 24));\nconst RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN || 18)\n);\nconst RL_EPISTEMIC_SEAL_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_EPISTEMIC_SEAL_PER_MIN || 120));\nconst RL_EPISTEMIC_LOG_PER_MIN = Math.max(20, Number(process.env.CASTLE_RL_EPISTEMIC_LOG_PER_MIN || 240));\nconst EPISTEMIC_SEAL_SECRET = String(\n process.env.CASTLE_EPISTEMIC_SEAL_SECRET || process.env.CASTLE_GATEWAY_TOKEN || \"\"\n).trim();\nconst RL_LLM_CONN_TEST_PER_MIN = Math.max(2, Number(process.env.CASTLE_RL_LLM_CONNECTION_TEST_PER_MIN || 8));\n\n/** One-shot production warnings: CORS surface, auth toggle, dev bypass flags (observatory deploy hygiene). */\nfunction logProductionObservatorySurfaceGuardsV0() {\n if (String(process.env.NODE_ENV || \"\").toLowerCase() !== \"production\") return;\n const tag = \"[GATEWAY_PROD_GUARD]\";\n const primaryCors = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n const corsListEmpty = ALLOWED_ORIGINS.length === 0;\n if (corsListEmpty && !primaryCors) {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty and CASTLE_HTTP_CORS_ORIGIN empty — production still merges RHIZOH_BROWSER_ORIGINS_V1 defaults; set env for preview/staging hosts`\n );\n } else if (corsListEmpty && primaryCors && primaryCors !== \"*\") {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty — only CASTLE_HTTP_CORS_ORIGIN set; add web.app + firebaseapp.com (and previews) to CASTLE_ALLOWED_ORIGINS if clients use alternate hostnames`\n );\n }\n if (primaryCors && /localhost|127\\.0\\.0\\.1/i.test(primaryCors)) {\n console.warn(`${tag} CORS: CASTLE_HTTP_CORS_ORIGIN is localhost while NODE_ENV=production`);\n }\n if (REQUIRE_AUTH) {\n console.warn(\n `${tag} AUTH: CASTLE_REQUIRE_AUTH=true — clients must send a valid gateway credential (otherwise silent 401 on replay/SSE); EventSource cannot set custom headers unless proxied`\n );\n if (REQUIRED_GATEWAY_TOKEN.length < 16) {\n console.warn(`${tag} AUTH: CASTLE_GATEWAY_TOKEN missing or short while require-auth enabled`);\n }\n }\n if (ALLOW_DEV_ANON) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_ANON is not \"false\" — dev anonymous path may be live in production (set CASTLE_ALLOW_DEV_ANON=false)`\n );\n }\n if (ALLOW_DEV_HTTP_UID) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_HTTP_UID is not \"false\" — X-Castle-Dev-Uid bypass risk (set CASTLE_ALLOW_DEV_HTTP_UID=false)`\n );\n }\n}\n\nconst rhizohRuntime = initRhizoh();\n\n/** İstemci: llmKeySource veya keySource — env | user_connection | auto */\nfunction normalizeClientLlmKeySource(raw) {\n const s = String(raw ?? \"\").trim().toLowerCase();\n if (s === \"env\" || s === \"server\") return \"env\";\n if (s === \"user_connection\" || s === \"connection\" || s === \"user\") return \"user_connection\";\n if (s === \"auto\" || s === \"\") return \"auto\";\n return null;\n}\n\n/**\n * Server-side provider wins for env/auto unless user_connection — client bundle may still send provider=openai.\n * @param {\"env\"|\"user_connection\"|\"auto\"} keyMode\n * @param {{ provider?: string, model?: string, apiKey?: string } | null} conn\n * @param {{ provider?: string, model?: string }} payload\n */\nfunction resolveServerLlmProviderV0(keyMode, conn, payload) {\n const envProvider = String(process.env.CASTLE_LLM_PROVIDER || \"\").trim() || undefined;\n const envModel = String(process.env.CASTLE_LLM_MODEL || \"\").trim() || undefined;\n\n if (keyMode === \"user_connection\") {\n return {\n resolvedProvider: conn?.provider || payload?.provider,\n resolvedModel: conn?.model || payload?.model,\n connApiKey: conn?.apiKey || \"\"\n };\n }\n\n if (keyMode === \"env\") {\n return {\n resolvedProvider: envProvider ?? payload?.provider,\n resolvedModel: envModel ?? payload?.model,\n connApiKey: \"\"\n };\n }\n\n if (conn?.provider && conn?.apiKey) {\n return {\n resolvedProvider: conn.provider,\n resolvedModel: conn.model,\n connApiKey: conn.apiKey\n };\n }\n\n return {\n resolvedProvider: envProvider ?? payload?.provider,\n resolvedModel: envModel ?? payload?.model,\n connApiKey: \"\"\n };\n}\n\nfunction jsonReplacerSafeV0(_key, value) {\n if (typeof value === \"bigint\") return Number(value);\n return value;\n}\n\nfunction sendJson(res, statusCode, payload) {\n res.writeHead(statusCode, { \"Content-Type\": \"application/json; charset=utf-8\" });\n try {\n res.end(JSON.stringify(payload, jsonReplacerSafeV0));\n } catch (error) {\n res.end(\n JSON.stringify({\n ok: false,\n error: \"response_serialize_failed\",\n detail: String(error?.message || error)\n })\n );\n }\n}\n\nfunction logRhizohHealth(stage, detail = {}) {\n try {\n console.info(`[CASTLE_${String(stage || \"unknown\")}]`, detail && typeof detail === \"object\" ? detail : {});\n } catch {\n /* noop */\n }\n}\n\nfunction renderInfraMetricsProm() {\n const lines = [\n `castle_gateway_events_processed_total ${infraMetrics.eventsProcessed}`,\n `castle_gateway_duplicate_reject_total ${infraMetrics.duplicateRejects}`,\n `castle_gateway_enqueue_latency_ms ${infraMetrics.enqueueLatencyMs}`,\n `castle_gateway_queue_depth ${infraMetrics.queueDepth}`,\n `castle_gateway_queue_lag_seconds ${(Number(infraMetrics.queueLag || 0) / 1000).toFixed(3)}`,\n `castle_gateway_errors_total ${infraMetrics.errors}`\n ];\n const buckets = infraMetrics.enqueueLatencyBuckets || {};\n for (const k of Object.keys(buckets)) {\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"${k}\"} ${buckets[k]}`);\n }\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"+Inf\"} ${infraMetrics.eventsProcessed}`);\n if (process.env.CASTLE_SUBSTRATE_METRICS !== \"0\") {\n lines.push(renderSubstratePrometheusMetricsV0().trimEnd());\n }\n return lines.join(\"\\n\");\n}\n\nasync function fetchJsonWithTimeout(url, timeoutMs = 1200) {\n const ctl = new AbortController();\n const tid = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const res = await fetch(url, { signal: ctl.signal });\n if (!res.ok) return null;\n return await res.json();\n } catch {\n return null;\n } finally {\n clearTimeout(tid);\n }\n}\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction getHttpPathname(req) {\n let p = String(req.url || \"\").split(\"?\")[0];\n /* Reverse proxies / clients sometimes append `/` — avoid silent 404 on e.g. `/rhizoh/genesis/stream/`. */\n while (p.length > 1 && p.endsWith(\"/\")) {\n p = p.slice(0, -1);\n }\n return p;\n}\n\nfunction rhizohLlmEnvConfigured() {\n const p = String(process.env.CASTLE_LLM_PROVIDER || \"openai\").toLowerCase();\n const key =\n p === \"anthropic\"\n ? process.env.ANTHROPIC_API_KEY\n : p === \"gemini\"\n ? process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY\n : p === \"xai\"\n ? process.env.XAI_API_KEY\n : p === \"deepseek\"\n ? process.env.DEEPSEEK_API_KEY\n : p === \"mistral\"\n ? process.env.MISTRAL_API_KEY\n : p === \"openrouter\"\n ? process.env.OPENROUTER_API_KEY\n : process.env.OPENAI_API_KEY;\n return !!String(key || \"\").trim();\n}\n\n/**\n * @param {import(\"firebase-admin/firestore\").Firestore | null} db\n * @param {number} timeoutMs\n */\nasync function probeFirestoreReachable(db, timeoutMs = 2000) {\n if (!db) return { ok: false, reason: \"no_db\" };\n const timeout = sleep(timeoutMs).then(() => ({ ok: false, reason: \"timeout\" }));\n const work = db\n .collection(\"_castle_gateway_health\")\n .doc(\"ping\")\n .get()\n .then(() => ({ ok: true, reason: \"reachable\" }))\n .catch((e) => ({ ok: false, reason: String(e?.message || e || \"firestore_error\") }));\n return Promise.race([work, timeout]);\n}\n\nfunction stripHtml(text = \"\") {\n return String(text)\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \" \")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \" \")\n .replace(/<[^>]+>/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction haversineMeters(aLat, aLon, bLat, bLon) {\n const R = 6371000;\n const dLat = ((bLat - aLat) * Math.PI) / 180;\n const dLon = ((bLon - aLon) * Math.PI) / 180;\n const sa = Math.sin(dLat / 2);\n const sb = Math.sin(dLon / 2);\n const x = sa * sa + Math.cos((aLat * Math.PI) / 180) * Math.cos((bLat * Math.PI) / 180) * sb * sb;\n return 2 * R * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));\n}\n\nasync function checkPublishTargetHealth(protocol, target) {\n const p = String(protocol || \"\").toUpperCase();\n const t = String(target || \"\").trim();\n if (!t) return { ok: false, reason: \"target_missing\", shouldReconnect: false };\n if (p === \"RTMP\") {\n const ok = t.startsWith(\"rtmp://\") || t.startsWith(\"rtmps://\");\n return { ok, reason: ok ? \"rtmp_target_format_ok\" : \"invalid_rtmp_target\", shouldReconnect: !ok, reconnectInSec: 15 };\n }\n if (p === \"WHIP\" || p === \"SFU\") {\n try {\n const res = await fetch(t, { method: \"OPTIONS\" });\n const ok = res.status < 500;\n return { ok, reason: `http_${res.status}`, shouldReconnect: !ok, reconnectInSec: 8 };\n } catch {\n return { ok: false, reason: \"network_unreachable\", shouldReconnect: true, reconnectInSec: 8 };\n }\n }\n return { ok: true, reason: \"unchecked_protocol\", shouldReconnect: false };\n}\n\nasync function fetchTextFromUrl(url) {\n const res = await fetch(url, { method: \"GET\" });\n if (!res.ok) throw new Error(`fetch_${res.status}`);\n const ct = String(res.headers.get(\"content-type\") || \"\");\n if (ct.includes(\"application/pdf\")) throw new Error(\"pdf_binary_not_supported_directly\");\n const body = await res.text();\n return stripHtml(body).slice(0, 14000);\n}\n\nfunction buildTaskActionGraph(task = \"\", context = {}) {\n const t = String(task || \"\").toLowerCase();\n if (t.includes(\"dinner\") || t.includes(\"akşam yemeği\") || t.includes(\"aksam yemegi\")) {\n return {\n taskTitle: \"Dinner Prep Planner\",\n nodes: [\n { id: \"n1\", title: \"Kitchen preheat\", action: \"smart-oven.preheat\", params: { temperatureC: 190, mode: \"fan\" } },\n { id: \"n2\", title: \"Lighting comfort\", action: \"smart-light.set_scene\", params: { scene: \"warm_dinner\" } },\n { id: \"n3\", title: \"Fridge inventory check\", action: \"smart-fridge.inventory_scan\", params: {} },\n { id: \"n4\", title: \"Reminder notification\", action: \"smart-watch.notify\", params: { text: \"Dinner prep started.\" } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" },\n { from: \"n3\", to: \"n4\" }\n ]\n };\n }\n return {\n taskTitle: task || \"General Planner\",\n nodes: [\n { id: \"n1\", title: \"Safety check\", action: \"device.safety_check\", params: { context } },\n { id: \"n2\", title: \"Primary action\", action: \"device.execute_task\", params: { task } },\n { id: \"n3\", title: \"Report\", action: \"studio.timeline_note\", params: { task } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" }\n ]\n };\n}\n\nfunction buildEthicsPrograms() {\n return {\n policyVersion: \"v1\",\n tracks: [\n {\n id: \"elderly-care-assist\",\n audience: \"elderly\",\n title: \"Elderly Assistance Track\",\n principles: [\"human-in-the-loop\", \"consent-first\", \"low-friction alerts\"],\n modules: [\"medication-reminder\", \"fall-risk-check\", \"family-notify\", \"daily-companion-brief\"]\n },\n {\n id: \"kids-safe-learning\",\n audience: \"children\",\n title: \"Kids Safe Learning Track\",\n principles: [\"guardian-consent\", \"age-appropriate-language\", \"content-filtering\"],\n modules: [\"study-coach\", \"screen-time-balance\", \"safe-social-share\", \"emergency-contact\"]\n }\n ]\n };\n}\n\nasync function deliverViaTelegram(channel, text) {\n const chatId = String(channel.endpoint || \"\").trim();\n if (!TELEGRAM_BOT_TOKEN) throw new Error(\"telegram_token_missing\");\n if (!chatId) throw new Error(\"telegram_chat_id_missing\");\n const url = `https://api.telegram.org/bot${encodeURIComponent(TELEGRAM_BOT_TOKEN)}/sendMessage`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ chat_id: chatId, text })\n });\n if (!res.ok) throw new Error(`telegram_http_${res.status}`);\n return { provider: \"telegram\", status: res.status };\n}\n\nasync function deliverViaWhatsapp(channel, text) {\n const to = String(channel.endpoint || \"\").trim();\n if (!WHATSAPP_TOKEN) throw new Error(\"whatsapp_token_missing\");\n if (!WHATSAPP_PHONE_NUMBER_ID) throw new Error(\"whatsapp_phone_number_id_missing\");\n if (!to) throw new Error(\"whatsapp_to_missing\");\n const url = `https://graph.facebook.com/v22.0/${encodeURIComponent(WHATSAPP_PHONE_NUMBER_ID)}/messages`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${WHATSAPP_TOKEN}` },\n body: JSON.stringify({ messaging_product: \"whatsapp\", to, type: \"text\", text: { body: text } })\n });\n if (!res.ok) throw new Error(`whatsapp_http_${res.status}`);\n return { provider: \"whatsapp\", status: res.status };\n}\n\nasync function deliverViaWebhook(channel, text) {\n const endpoint = String(channel.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"webhook_endpoint_missing\");\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text, platform: channel.platform || \"webhook\", ts: Date.now() })\n });\n if (!res.ok) throw new Error(`webhook_http_${res.status}`);\n return { provider: \"webhook\", status: res.status };\n}\n\nasync function deliverSocialMessage(channel, text) {\n const platform = String(channel.platform || \"webhook\").toLowerCase();\n if (platform === \"telegram\") return deliverViaTelegram(channel, text);\n if (platform === \"whatsapp\") return deliverViaWhatsapp(channel, text);\n return deliverViaWebhook(channel, text);\n}\n\nasync function deliverSocialWithRetry(channel, text) {\n let attempt = 0;\n let lastError = null;\n while (attempt <= SOCIAL_RETRY_MAX) {\n try {\n const result = await deliverSocialMessage(channel, text);\n return { ok: true, attempt: attempt + 1, ...result };\n } catch (error) {\n lastError = error;\n if (attempt >= SOCIAL_RETRY_MAX) break;\n const jitter = Math.floor(Math.random() * 90);\n await sleep(SOCIAL_RETRY_BASE_MS * Math.pow(2, attempt) + jitter);\n }\n attempt += 1;\n }\n return { ok: false, attempt: attempt + 1, error: String(lastError?.message || \"delivery_failed\") };\n}\n\nasync function dispatchCommandToAdapter(device, command) {\n const adapter = String(command?.adapter || device?.adapter || \"websocket\").toLowerCase();\n const endpoint = String(device?.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"device_endpoint_missing\");\n if (adapter === \"mqtt\") {\n // MQTT delivery is proxied via a bridge endpoint (HTTP webhook style).\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n type: \"mqtt-command\",\n topic: `castle/devices/${device.id}/commands`,\n action: command.action,\n params: command.params || {}\n })\n });\n if (!res.ok) throw new Error(`mqtt_bridge_http_${res.status}`);\n return { ok: true, adapter, via: \"http-bridge\", status: res.status };\n }\n if (!endpoint.startsWith(\"ws://\") && !endpoint.startsWith(\"wss://\")) throw new Error(\"invalid_websocket_endpoint\");\n const sendResult = await new Promise((resolve, reject) => {\n const ws = new WebSocket(endpoint);\n const timer = setTimeout(() => {\n try {\n ws.close();\n } catch {\n /* noop */\n }\n reject(new Error(\"websocket_timeout\"));\n }, 3000);\n ws.on(\"open\", () => {\n ws.send(\n JSON.stringify({\n type: \"castle-command\",\n commandId: command.id,\n deviceId: command.deviceId,\n action: command.action,\n params: command.params || {}\n }),\n (err) => {\n clearTimeout(timer);\n ws.close();\n if (err) reject(new Error(\"websocket_send_failed\"));\n else resolve({ ok: true, adapter: \"websocket\", via: \"ws\" });\n }\n );\n });\n ws.on(\"error\", () => {\n clearTimeout(timer);\n reject(new Error(\"websocket_connect_failed\"));\n });\n });\n return sendResult;\n}\n\nfunction readHttpJson(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => {\n if (!buf) return resolve({});\n const payload = safeJsonParse(buf);\n if (!payload || typeof payload !== \"object\") return reject(new Error(\"invalid_json_payload\"));\n resolve(payload);\n });\n req.on(\"error\", reject);\n });\n}\n\n/** Ham gövde (HMAC doğrulaması için). */\nfunction readHttpBodyText(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => resolve(buf));\n req.on(\"error\", reject);\n });\n}\n\nfunction maskOpaqueId(value) {\n const t = String(value || \"\");\n if (!t) return \"(empty)\";\n if (t.length <= 8) return \"****\";\n return `${t.slice(0, 4)}…${t.slice(-2)}`;\n}\n\nfunction readGatewayHttpToken(req) {\n const x = String(req.headers?.[\"x-castle-gateway-token\"] || \"\").trim();\n if (x) return x;\n const auth = String(req.headers?.authorization || \"\");\n const m = /^Bearer\\s+(.+)$/i.exec(auth);\n return m ? String(m[1] || \"\").trim() : \"\";\n}\n\nasync function resolveHttpUser(req) {\n const authResult = await verifyClientToken(req);\n if (authResult?.ok && authResult.user?.uid) {\n return { ok: true, uid: String(authResult.user.uid), mode: authResult.kind || \"auth\" };\n }\n if (ALLOW_DEV_ANON && ALLOW_DEV_HTTP_UID) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) return { ok: true, uid: `dev-${devUid.slice(0, 64)}`, mode: \"dev-header\" };\n }\n return { ok: false, reason: authResult?.reason || \"auth_required\" };\n}\n\n/**\n * Epistemic ledger ingest — Firebase uid OR valid transport token + X-Castle-Dev-Uid.\n * Prod disables CASTLE_ALLOW_DEV_HTTP_UID; LLM may still work while telemetry 401'd without this.\n */\nasync function resolveEpistemicIngestActor(req) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok && tok === REQUIRED_GATEWAY_TOKEN) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) {\n return { ok: true, uid: `gw-telemetry-${devUid.slice(0, 64)}`, mode: \"gateway-token-dev-uid\" };\n }\n return { ok: false, reason: \"dev_uid_required\" };\n }\n if (tok) {\n return { ok: false, reason: \"gateway_token_mismatch\" };\n }\n }\n const auth = await resolveHttpUser(req);\n if (auth.ok) return auth;\n return { ok: false, reason: auth.reason || \"auth_required\" };\n}\n\n/** Mesh actor: Firebase uid, dev header, or `guest:<uuid>` via header / `guestId` query (SSE cannot set headers). */\nasync function resolveMeshActor(req) {\n const auth = await resolveHttpUser(req);\n if (auth.ok) return { ok: true, clientUid: auth.uid, mode: auth.mode };\n if (ALLOW_MESH_GUEST) {\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const gid = String(req.headers?.[\"x-castle-guest-id\"] || q.searchParams.get(\"guestId\") || \"\").trim();\n if (gid) {\n const normalized = gid.startsWith(\"guest:\") ? gid.slice(0, 180) : `guest:${gid.slice(0, 160)}`;\n return { ok: true, clientUid: normalized, mode: \"guest-header\" };\n }\n }\n return { ok: false, reason: \"mesh_auth_required\" };\n}\n\n/** Live genesis runtime JSON (GET /rhizoh/genesis/runtime + SSE infra sampler). */\nasync function buildGenesisRuntimeSurfacePayloadLive() {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n let spiralWsActive = 0;\n if (spiralWssForGenesis) {\n for (const c of spiralWssForGenesis.clients) {\n if (c.readyState === 1) spiralWsActive += 1;\n }\n }\n const base = buildGenesisRuntimeSurfacePayload({\n infraMetrics,\n scoredHealth: scored,\n rhizohEnterpriseMetrics,\n mesh: meshContinuityAggregate(),\n workerHealth: worker,\n port: PORT,\n spiralWebSocketClientsActive: spiralWsActive\n });\n return {\n ...base,\n replayFingerprint: computeGenesisReplayFingerprintV0(base),\n gatewayCapabilities: buildGenesisGatewayCapabilitiesV0(),\n genesisStream: {\n schema: \"castle.genesis.stream_cursor.v0\",\n lastAcceptedSeq: getGenesisContinuitySeq(),\n clientIngress: {\n schema: GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n contract: \"castle.world_observation.ingress_envelope.v1\",\n postPath: rhizohRuntime.routes.genesisIngress,\n closure: getGenesisWorldObservationIngressStatusV0()\n },\n seqContinuity: getGenesisContinuitySeqAuditV0(),\n eventArchive: genesisContinuityEventArchiveEnabled()\n ? {\n enabled: true,\n projection: GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION,\n queryPath: rhizohRuntime.routes.genesisContinuityEvents\n }\n : { enabled: false },\n replayRouter: {\n schema: GENESIS_REPLAY_ROUTER_SCHEMA,\n queryPath: rhizohRuntime.routes.genesisReplay,\n queryHint: \"from=&to= or range=from-to; optional type=&checkpoints=0\",\n temporalDiffPath: rhizohRuntime.routes.genesisReplayDiff,\n temporalDiffSchema: GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA,\n equivalencePath: rhizohRuntime.routes.genesisReplayEquivalence,\n analyticsPath: rhizohRuntime.routes.genesisReplayAnalytics,\n evolutionPath: rhizohRuntime.routes.genesisReplayEvolution\n }\n }\n };\n}\n\nconst httpServer = createServer(async (req, res) => {\n const corsAllow = applyHttpCorsHeaders(req, res);\n if (req.method === \"OPTIONS\") {\n if (!corsAllow && req.headers?.origin) {\n console.warn(\"[GATEWAY_CORS] preflight rejected for origin:\", req.headers.origin);\n }\n res.writeHead(corsAllow ? 204 : 403);\n res.end();\n return;\n }\n\n const pathname = getHttpPathname(req);\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/genesis/__ping\") {\n sendJson(res, 200, {\n ok: true,\n pid: process.pid,\n entry: __filename,\n time: Date.now(),\n genesis: true\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/epistemic/__probe\") {\n sendJson(res, 200, {\n ok: true,\n epistemicIngestActorV2: true,\n routes: {\n seal: rhizohRuntime.routes.epistemicSeal,\n logsBatch: rhizohRuntime.routes.epistemicLogsBatch\n },\n hasGatewayToken: Boolean(REQUIRED_GATEWAY_TOKEN),\n gatewayTokenLen: REQUIRED_GATEWAY_TOKEN ? REQUIRED_GATEWAY_TOKEN.length : 0\n });\n return;\n }\n\n if (\n req.method === \"GET\" &&\n (pathname === \"/rhizoh/live/world-feed\" ||\n pathname === \"/rhizoh/live/sports-bundle\" ||\n pathname === \"/rhizoh/live/news-headlines\")\n ) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_live_world_feed:${ip}`, RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const out =\n pathname === \"/rhizoh/live/sports-bundle\"\n ? await handleLiveSportsBundleGetV0()\n : pathname === \"/rhizoh/live/news-headlines\"\n ? await handleLiveNewsHeadlinesGetV0(req)\n : await handleLiveWorldFeedGetV0(req);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/webhooks/stripe\") {\n await handleStripeMembershipWebhook(req, res);\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/billing/stripe/checkout\") {\n await handleStripeCheckoutCreate(req, res, { readHttpJson, resolveHttpUser, sendJson });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/live\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, live: false, dns: true, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n sendJson(res, 200, { ok: true, live: true, dns: true, service: \"castle-gateway\", ts: Date.now() });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/cors\") {\n const reqOrigin = normalizeHttpOrigin(req.headers?.origin);\n const allowOrigin = accessControlAllowOriginValue(req);\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n requestOrigin: reqOrigin || null,\n allowed: Boolean(allowOrigin),\n allowOrigin: allowOrigin || null,\n allowSetSize: httpCors.allowSet.size\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/ready\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, ready: false, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n try {\n const persistence = getFirebasePersistence();\n const llmProbe = rhizohLlmEnvConfigured();\n sendJson(res, 200, {\n ok: true,\n ready: true,\n dns: true,\n persistence: persistence.mode,\n service: \"castle-gateway\",\n gateway: \"ok\",\n llm: llmProbe ? \"ok\" : \"degraded\",\n spine: llmProbe ? \"ok\" : \"partial\"\n });\n } catch (e) {\n sendJson(res, 503, { ok: false, ready: false, reason: String(e?.message || e), service: \"castle-gateway\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/reality\") {\n const payload = buildRealityHealthPayload();\n sendJson(res, payload.ok ? 200 : 503, payload);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/substrate\") {\n const authority = buildGatewaySubstrateAuthoritySnapshotV0();\n const operational = buildSubstrateOperationalSnapshotV0();\n const drift = buildRealityHealthPayload();\n const ok = Boolean(authority.productionAuthoritySatisfied) && drift.ok;\n sendJson(res, ok ? 200 : 503, {\n ok,\n service: \"castle-gateway\",\n ts: Date.now(),\n authority,\n operational,\n drift: { ok: drift.ok, errors: drift.drift?.errors, warnings: drift.drift?.warnings }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.councilAnomalyReasoning) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_council_anomaly:${ip}`, 30, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const body = await readHttpJson(req);\n const out = handleCouncilAnomalyReasoningPostV0(body);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/substrate/health\") {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const body = await readHttpJson(req);\n const uid = (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestClientSubstrateHealthV0(body, { uid, clientId: uid });\n if (!result.ok) {\n sendJson(res, 400, result);\n return;\n }\n recordSubstrateOtelEventV0(\"client_health_ingest\", { uid: String(uid).slice(0, 32) });\n sendJson(res, 200, result);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/deps\") {\n const t0 = Date.now();\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, {\n ok: false,\n dns: true,\n llm: false,\n firestore: false,\n phase: \"maintenance\",\n latencyMs: Date.now() - t0,\n service: \"castle-gateway\"\n });\n return;\n }\n const persistence = getFirebasePersistence();\n let firestoreOk = persistence.mode !== \"firebase\";\n let firestoreDetail = persistence.mode === \"file\" ? \"file_mode\" : \"not_applicable\";\n if (persistence.mode === \"firebase\" && persistence.db) {\n const pr = await probeFirestoreReachable(persistence.db, 2000);\n firestoreOk = pr.ok;\n firestoreDetail = pr.reason || (pr.ok ? \"reachable\" : \"fail\");\n } else if (persistence.mode === \"firebase\" && !persistence.db) {\n firestoreOk = false;\n firestoreDetail = \"firebase_not_initialized\";\n }\n const llmOk = rhizohLlmEnvConfigured();\n const overallOk = llmOk && firestoreOk;\n const latencyMs = Date.now() - t0;\n sendJson(res, 200, {\n ok: overallOk,\n dns: true,\n llm: llmOk,\n firestore: persistence.mode === \"firebase\" ? firestoreOk : false,\n persistence: persistence.mode,\n firestoreDetail,\n latencyMs,\n service: \"castle-gateway\",\n wsPort: PORT\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health\") {\n const persistence = getFirebasePersistence();\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n wsPort: PORT,\n persistence: persistence.mode,\n presenceMesh: \"sse\"\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/health\") {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n sendJson(res, 200, {\n ok: true,\n role: \"gateway\",\n queueLagMs: infraMetrics.queueLag,\n queueDepth: infraMetrics.queueDepth,\n errors: infraMetrics.errors,\n status: scored.status,\n score: scored.score,\n reasons: scored.reasons,\n worker: worker\n ? {\n status: worker.status,\n score: worker.score,\n reasons: worker.reasons\n }\n : null,\n timestamp: Date.now()\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisRuntime) {\n const payload = await buildGenesisRuntimeSurfacePayloadLive();\n sendJson(res, 200, payload);\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.genesisIngress) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const actor = await resolveMeshActor(req);\n const clientId = actor.ok ? actor.clientUid : (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestGenesisWorldObservationV0(body, { clientId });\n if (!result.ok) {\n const status =\n result.error === \"rate_limited\" || result.error === \"agent_spoke_throttled\"\n ? 429\n : result.error === \"payload_too_large\"\n ? 413\n : 400;\n sendJson(res, status, result);\n return;\n }\n sendJson(res, result.idempotent ? 200 : 202, result);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisStream) {\n const corsOrigin = accessControlAllowOriginValue(req);\n if (!corsOrigin && String(process.env.NODE_ENV || \"\").toLowerCase() === \"production\") {\n const og = String(req.headers?.origin || \"\").trim();\n if (og) {\n console.warn(\"[GATEWAY] genesis SSE: missing Access-Control-Allow-Origin for browser Origin (CORS whitelist mismatch):\", og);\n }\n }\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n res.write(\"retry: 3000\\n\\n\");\n res.write(\": genesis continuity stream\\n\\n\");\n res.write(\"event: genesis\\n\");\n res.write(`data: {\"booted\":true,\"pid\":${process.pid}}\\n\\n`);\n registerGenesisSseClient(res, req);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLatest) {\n const cp = getLatestGenesisSignedCheckpoint();\n if (!cp) {\n sendJson(res, 404, { ok: false, error: \"no_checkpoint_yet\", hint: \"wait_for_seq_interval_or_configure_signing_secret\" });\n return;\n }\n sendJson(res, 200, { ok: true, checkpoint: cp });\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(rhizohRuntime.routes.genesisCheckpointBySeqPrefix)) {\n const tail = pathname.slice(rhizohRuntime.routes.genesisCheckpointBySeqPrefix.length).split(\"/\")[0];\n const seq = parseInt(String(tail || \"\").trim(), 10);\n const out = await genesisCheckpointQueryBySeqV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"checkpoint_not_found\") {\n sendJson(res, 404, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointRange) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryRangeV0(from, to);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"range_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLineage) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const seq = parseInt(String(u.searchParams.get(\"seq\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryLineageV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"lineage_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisContinuityEvents) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const limit = parseInt(String(u.searchParams.get(\"limit\") || \"\").trim(), 10);\n const out = await queryGenesisContinuityEventArchiveV0(from, to, type, limit);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\" || out.error === \"event_archive_disabled\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"archive_file_too_large\") {\n sendJson(res, 413, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplay) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await resolveGenesisReplayRouterV1({\n from: fromSeq,\n to: toSeq,\n type,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayDiff) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const out = await computeGenesisReplayTemporalDiffV1({ from: fromSeq, to: toSeq, type });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEquivalence) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from1 = parseInt(String(u.searchParams.get(\"from1\") || \"\").trim(), 10);\n const to1 = parseInt(String(u.searchParams.get(\"to1\") || \"\").trim(), 10);\n const from2 = parseInt(String(u.searchParams.get(\"from2\") || \"\").trim(), 10);\n const to2 = parseInt(String(u.searchParams.get(\"to2\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await compareGenesisReplayEquivalenceV1({\n from1,\n to1,\n from2,\n to2,\n type,\n includeCheckpoints\n });\n if (\n !out.ok &&\n (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")\n ) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayAnalytics) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayAnalyticsV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEvolution) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const collapseWindows = parseInt(String(u.searchParams.get(\"collapseWindows\") || \"0\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayEvolutionV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n collapseWindows,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/metrics\") {\n res.writeHead(200, { \"Content-Type\": \"text/plain; version=0.0.4; charset=utf-8\" });\n res.end(renderInfraMetricsProm());\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.infraPrometheusMetrics) {\n if (process.env.CASTLE_PROMETHEUS_METRICS !== \"1\") {\n sendJson(res, 404, { ok: false, error: \"prometheus_metrics_disabled\" });\n return;\n }\n res.writeHead(200, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(renderRhizohPrometheusMetrics());\n return;\n }\n if (req.method === \"GET\" && pathname.startsWith(\"/infra/traces/\")) {\n const traceId = decodeURIComponent(pathname.slice(\"/infra/traces/\".length));\n const item = await getTraceById(traceId);\n if (!item) return sendJson(res, 404, { ok: false, error: \"trace_not_found\" });\n return sendJson(res, 200, { ok: true, trace: item });\n }\n if (req.method === \"GET\" && pathname === \"/infra/traces\") {\n const u = new URL(String(req.url || \"/infra/traces\"), `http://${req.headers.host || \"localhost\"}`);\n const sessionId = String(u.searchParams.get(\"sessionId\") || \"\").trim();\n const items = sessionId ? await listTracesBySession(sessionId) : [];\n return sendJson(res, 200, { ok: true, sessionId, traces: items });\n }\n\n /** Presence room mesh (C): JOIN / LEAVE / SNAPSHOT / DELTA / REPLAY (HTTP); SUBSCRIBE = SSE stream. */\n if (req.method === \"POST\" && pathname === \"/presence/mesh/join\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid, clientUid: actor.clientUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/leave\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshLeave(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/snapshot\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n sendJson(res, 200, meshSnapshot(roomUid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/delta\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, MAX_MESSAGE_BYTES);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const seq = meshAppendDelta(roomUid, actor.clientUid, {\n node: body?.node,\n projectionPatch: body?.projectionPatch,\n writerSubject: body?.writerSubject\n });\n sendJson(res, 200, { ok: true, roomUid, seq });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/replay\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const fromNodeId = body?.fromNodeId != null ? String(body.fromNodeId) : \"\";\n const fromSeq = body?.fromSeq;\n if (fromNodeId) {\n sendJson(res, 200, meshReplayFromNodeId(roomUid, fromNodeId));\n return;\n }\n sendJson(res, 200, meshReplayFromSeq(roomUid, fromSeq));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/presence/mesh/subscribe\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const roomUid = String(q.searchParams.get(\"roomUid\") || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n const write = (chunk) => {\n try {\n res.write(chunk);\n } catch {\n /* closed */\n }\n };\n const unsub = meshSubscribeSse(roomUid, write);\n const onClose = () => {\n unsub();\n meshLeave(roomUid, actor.clientUid);\n try {\n res.end();\n } catch {\n /* ignore */\n }\n };\n req.on(\"close\", onClose);\n req.on(\"aborted\", onClose);\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, uid: auth.uid, mode: auth.mode, items: await listConnections(auth.uid) });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const id = await createConnection(auth.uid, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"create_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n await updateConnection(auth.uid, id, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"update_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"DELETE\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n await deleteConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"delete_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url?.startsWith(\"/rhizoh/network/presence\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const u = new URL(req.url, \"http://localhost\");\n const roomKey = String(u.searchParams.get(\"room\") || \"world_space_c2c_v0\").slice(0, 64);\n sendJson(res, 200, {\n ok: true,\n roomKey,\n presence: listCastleNetworkPresenceV0(roomKey)\n });\n } catch (error) {\n sendJson(res, 500, { ok: false, error: error?.message || \"presence_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/rhizoh/sync/vault\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const snapshot = await getCloudSyncSnapshotV0(auth.uid);\n sendJson(res, 200, { ok: true, uid: auth.uid, snapshot });\n } catch (error) {\n sendJson(res, 500, { ok: false, error: error?.message || \"cloud_sync_pull_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/rhizoh/sync/vault\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const snapshot = await mergeCloudSyncSnapshotV0(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, uid: auth.uid, snapshot });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"cloud_sync_push_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections/test\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n if (!checkHttpRateLimit(`llm_conn_test:${auth.uid}`, RL_LLM_CONN_TEST_PER_MIN, 60_000)) {\n return sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n }\n const hasBodyKey = !!String(payload?.apiKey || \"\").trim();\n const result = await queryRhizohLlm(\n {\n message: String(payload?.message || \"Ping from Castle Gateway\"),\n context: payload?.context || { source: \"connection_test\" },\n provider: payload?.provider,\n model: payload?.model,\n apiKey: payload?.apiKey || \"\"\n },\n { allowExternalApiKey: true, llmKeySource: hasBodyKey ? \"auto\" : \"env\" }\n );\n logLlmAccess({\n route: \"/llm/connections/test\",\n uid: auth.uid,\n llmKeyBillingOwner: result.llmKeyBillingOwner,\n llmKeyOrigin: result.llmKeyOrigin,\n provider: result.provider,\n model: result.model,\n connectionId: null\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"connection_test_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAgentIdentities(auth.uid);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerAgentIdentity({\n ownerUid: auth.uid,\n agentId: payload?.agentId,\n role: payload?.role,\n personaSeed: payload?.personaSeed || {},\n capabilityLevel: payload?.capabilityLevel\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/agents/identities/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const owns = await getAgentIdentity(id);\n if (!owns || owns.ownerUid !== auth.uid) return sendJson(res, 403, { ok: false, error: \"forbidden\" });\n const payload = await readHttpJson(req);\n const row = await updateAgentIdentity(id, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/academy/events\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/academy/events\"}`);\n const status = String(url.searchParams.get(\"status\") || \"\");\n const items = await listAcademyEvents({ ownerUid: auth.uid, status, limit: 120 });\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/academy/events\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const ev = await queueAcademyEvent({\n type: payload?.type,\n roomId: payload?.roomId,\n ownerUid: auth.uid,\n participants: payload?.participants || [],\n topic: payload?.topic || \"academy\",\n payload: payload?.payload || {}\n });\n sendJson(res, 200, { ok: true, event: ev });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && String(req.url || \"\").startsWith(\"/academy/events/\") && String(req.url || \"\").endsWith(\"/resolve\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const parts = String(req.url || \"\").split(\"/\").filter(Boolean);\n const eventId = parts[2] || \"\";\n try {\n const payload = await readHttpJson(req);\n const out = await resolveAcademyEvent(eventId, payload || {});\n sendJson(res, 200, { ok: true, event: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_resolve_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/memory\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/memory\"}`);\n const scope = url.searchParams.get(\"scope\") === \"agents\" ? \"agents\" : \"users\";\n const id = String(url.searchParams.get(\"id\") || (scope === \"agents\" ? \"\" : auth.uid));\n const kind = String(url.searchParams.get(\"kind\") || \"\");\n const limit = Number(url.searchParams.get(\"limit\") || 40);\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n sendJson(res, 200, { ok: true, scope, id, kind, items: await listMemories({ scope, id, kind, limit }) });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const profile = await getPersonaGoalMemory(auth.uid);\n sendJson(res, 200, { ok: true, profile });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const profile = await setPersonaGoalMemory({\n uid: auth.uid,\n personaPatch: payload?.personaPatch || {},\n goals: Array.isArray(payload?.goals) ? payload.goals : null,\n preferencesPatch: payload?.preferencesPatch || {}\n });\n sendJson(res, 200, { ok: true, profile });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"profile_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/compact\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const out = await autoCompactMemories({ scope, id });\n sendJson(res, 200, { ok: true, scope, id, ...out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_compact_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/context\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const context = await getMemoryContext({\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n query: String(payload?.query || \"\"),\n limit: Number(payload?.limit || 80)\n });\n sendJson(res, 200, { ok: true, context });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_context_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/place-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const placeName = String(payload?.placeName || \"\").trim();\n if (!placeName) return sendJson(res, 400, { ok: false, error: \"placeName_required\" });\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const persona = String(payload?.waypointPersona || payload?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(placeName)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Place: ${placeName}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Give a concise conversational briefing about this place for a walking-route companion.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-place-brief\", placeName, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `PLACE_BRIEF:${placeName} => ${String(summary.reply || \"\").slice(0, 900)}`,\n tags: [\"place\", \"brief\", \"event-layer\"],\n importance: 0.62,\n meta: { placeName }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"place-brief\",\n text: `${placeName} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: {\n placeName,\n directive: summary.directive,\n lat: hit?.lat ? Number(hit.lat) : null,\n lon: hit?.lon ? Number(hit.lon) : null\n }\n });\n sendJson(res, 200, { ok: true, place: hit || null, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"place_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/route-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const waypoints = Array.isArray(payload?.waypoints) ? payload.waypoints.slice(0, 24) : [];\n const out = [];\n for (const wp of waypoints) {\n const name = String(wp?.name || \"\").trim();\n if (!name) continue;\n const persona = String(wp?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(name)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Waypoint: ${name}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Generate a concise spoken briefing for route waypoint.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-route-brief\", waypoint: name, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n out.push({\n name,\n persona,\n place: hit || null,\n reply: summary.reply,\n directive: summary.directive\n });\n }\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `ROUTE_BRIEF: waypoints=${out.length} ${out.map((x) => `${x.name}:${x.persona}`).join(\" | \")}`.slice(0, 1300),\n tags: [\"route\", \"waypoint\", \"brief\", \"event-layer\"],\n importance: 0.74,\n meta: { count: out.length }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"route-brief\",\n text: out.map((x) => `${x.name}:${String(x.reply || \"\").slice(0, 140)}`).join(\" | \").slice(0, 1400),\n roomId: \"studio-main\",\n meta: {\n waypointCount: out.length,\n waypoints: out.map((x) => ({\n name: x.name,\n persona: x.persona,\n lat: x.place?.lat ? Number(x.place.lat) : null,\n lon: x.place?.lon ? Number(x.place.lon) : null\n }))\n }\n });\n sendJson(res, 200, { ok: true, items: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"route_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/pdf-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const title = String(payload?.title || \"document\");\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const providedText = String(payload?.text || \"\");\n const url = String(payload?.url || \"\");\n let content = providedText;\n if (!content && url) content = await fetchTextFromUrl(url);\n if (!content) return sendJson(res, 400, { ok: false, error: \"text_or_supported_url_required\" });\n const summary = await queryRhizohLlm(\n {\n message: `Summarize this document and provide key points + practical actions.\\nTitle:${title}\\nContent:\\n${content.slice(0, 12000)}`,\n context: { source: \"event-layer-pdf-brief\", title },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `DOC_BRIEF:${title} => ${String(summary.reply || \"\").slice(0, 1000)}`,\n tags: [\"document\", \"brief\", \"library\", \"event-layer\"],\n importance: 0.7,\n meta: { title, url: url || null }\n });\n await addTranscript(auth.uid, {\n source: \"library-brief\",\n eventType: \"pdf-brief\",\n text: `${title} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { title }\n });\n sendJson(res, 200, { ok: true, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/pdf/upload\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req, 8 * 1024 * 1024);\n const job = await enqueuePdfJob({\n uid: auth.uid,\n fileName: payload?.fileName || \"document.pdf\",\n bytesBase64: payload?.contentBase64\n });\n sendJson(res, 200, { ok: true, job });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_upload_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/session\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createPublishSession(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session\",\n text: `Publish session armed: ${row.protocol} ${row.target}`.slice(0, 500),\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: row.id, bridge: row.bridge }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/studio/publish/sessions\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listPublishSessions(auth.uid, 80);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/health-check\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const protocol = String(payload?.protocol || \"WHIP\");\n const target = String(payload?.target || \"\");\n const health = await checkPublishTargetHealth(protocol, target);\n if (payload?.sessionId) {\n await updatePublishSession(auth.uid, String(payload.sessionId), {\n status: health.ok ? \"healthy\" : \"degraded\",\n reconnectPolicy: {\n enabled: true,\n backoffSec: [2, 5, 10, 20],\n nextRetryInSec: health.reconnectInSec || 0,\n maxRetries: 8,\n reason: health.reason\n }\n });\n }\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-health\",\n text: `${protocol} ${target} => ${health.ok ? \"healthy\" : \"degraded\"} (${health.reason})`,\n roomId: \"studio-main\",\n meta: { protocol, target, reconnect: !!health.shouldReconnect }\n });\n sendJson(res, 200, { ok: true, health });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_health_check_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/studio/publish/sessions/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updatePublishSession(auth.uid, id, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session-update\",\n text: `Session ${id} -> ${row.status || \"updated\"}`,\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: id, status: row.status, ingestUrl: row.ingestUrl || \"\" }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/transcripts\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/transcripts\"}`);\n const limit = Math.max(1, Math.min(200, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await listTranscripts(auth.uid, limit);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/transcripts\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await addTranscript(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"transcript_write_failed\" });\n }\n return;\n }\n\n /** E1: broadcast intent → GreenRoom capability → transcript + memory artifact */\n if (req.method === \"POST\" && req.url === \"/studio/capabilities/greenroom\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const traceId = String(payload?.traceId || `TRC-${Date.now().toString(36).toUpperCase()}`);\n const title = String(payload?.title || \"Castle Live Broadcast\").slice(0, 200);\n const intentRaw = String(payload?.intentRaw || \"\").slice(0, 800);\n const audienceEstimate = Math.max(0, Math.min(50_000_000, Number(payload?.audienceEstimate) || 1200));\n const roomId = String(payload?.roomId || \"greenroom-main\");\n const transcriptRow = await addTranscript(auth.uid, {\n source: \"rhizoh\",\n eventType: \"BROADCAST_ROUTED\",\n text: `GreenRoom · ${title}${intentRaw ? `\\nIntent: ${intentRaw}` : \"\"}`,\n roomId,\n meta: {\n ack: \"BROADCAST_ROUTED\",\n capability: \"greenroom\",\n intentType: \"broadcast\",\n traceId,\n audienceEstimate,\n artifact: {\n kind: \"broadcast_card\",\n title,\n traceId,\n roomId,\n createdAt: Date.now()\n }\n }\n });\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n text: `GREENROOM_BROADCAST:${title} trace=${traceId}`,\n tags: [\"broadcast\", \"greenroom\", \"artifact\", \"castle-library\"],\n importance: 0.72,\n kind: \"episodic\",\n meta: { traceId, capability: \"greenroom\", transcriptId: transcriptRow.id }\n });\n sendJson(res, 200, {\n ok: true,\n ack: \"BROADCAST_ROUTED\",\n traceId,\n transcript: transcriptRow,\n replayPath: `/replay/${encodeURIComponent(traceId)}`,\n sharePath: `/replay/${encodeURIComponent(traceId)}`\n });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"greenroom_capability_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/metadata/search\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/metadata/search\"}`);\n const q = String(url.searchParams.get(\"q\") || \"\");\n const limit = Math.max(1, Math.min(300, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await searchMetadata(auth.uid, q, limit);\n sendJson(res, 200, { ok: true, q, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerDevice(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, device: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"device_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const lat = Number(payload?.lat);\n const lon = Number(payload?.lon);\n const speed = Math.max(0, Number(payload?.speed || 0));\n const cfgCenterLat = Number(process.env.CASTLE_GEOFENCE_CENTER_LAT || 41.0082);\n const cfgCenterLon = Number(process.env.CASTLE_GEOFENCE_CENTER_LON || 28.9784);\n const cfgRadiusM = Math.max(100, Number(process.env.CASTLE_GEOFENCE_RADIUS_M || 55000));\n const cfgSpeedCap = Math.max(1, Number(process.env.CASTLE_SPEED_CAP_MPS || 35));\n const dist = Number.isFinite(lat) && Number.isFinite(lon) ? haversineMeters(cfgCenterLat, cfgCenterLon, lat, lon) : 0;\n const geofenceOk = !Number.isFinite(dist) ? true : dist <= cfgRadiusM;\n const speedOk = speed <= cfgSpeedCap;\n const failsafe = !(geofenceOk && speedOk);\n const safety = {\n geofenceOk,\n speedOk,\n failsafe,\n distanceFromCenterM: Number.isFinite(dist) ? dist : null,\n speedCapMps: cfgSpeedCap\n };\n const row = await appendTelemetry(auth.uid, deviceId, {\n lat,\n lon,\n speed,\n battery: payload?.battery,\n mode: payload?.mode || (failsafe ? \"FAILSAFE_HOLD\" : \"ACTIVE\"),\n raw: payload?.raw || {}\n });\n await updateDevice(auth.uid, deviceId, {\n status: failsafe ? \"failsafe\" : \"active\",\n lastTelemetryAt: row.ts,\n safetyState: safety\n });\n if (failsafe) {\n await addTranscript(auth.uid, {\n source: \"robotics-guardrails\",\n eventType: \"failsafe\",\n text: `Device ${deviceId} failsafe triggered geofenceOk=${geofenceOk} speedOk=${speedOk}`,\n roomId: \"studio-main\",\n meta: { deviceId, safety }\n });\n }\n sendJson(res, 200, { ok: true, telemetry: row, safety, command: failsafe ? \"FAILSAFE_HOLD\" : \"CONTINUE\" });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"telemetry_ingest_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listTelemetry(auth.uid, 150);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createAutomation(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"automation-scheduled\",\n text: `Automation scheduled: ${row.title} at ${new Date(row.scheduleAt).toLocaleString()}`,\n roomId: \"studio-main\",\n meta: { automationId: row.id, action: row.action, deviceId: row.deviceId }\n });\n sendJson(res, 200, { ok: true, automation: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"automation_create_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAutomations(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listDevices(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const devs = await listDevices(auth.uid, 500);\n const d = devs.find((x) => x.id === deviceId);\n if (!d) return sendJson(res, 404, { ok: false, error: \"device_not_found\" });\n const action = String(payload?.action || \"\");\n const caps = Array.isArray(d.capabilityProfile) ? d.capabilityProfile : [];\n if (action && caps.length && !caps.includes(action) && !caps.includes(\"*\")) {\n return sendJson(res, 400, { ok: false, error: \"unsupported_action_for_device\", capabilityProfile: caps });\n }\n const cmd = await enqueueCommand(auth.uid, {\n deviceId,\n action: action || \"NOOP\",\n params: payload?.params || {},\n adapter: payload?.adapter || d.adapter || \"websocket\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-command-queue\",\n eventType: \"command-queued\",\n text: `${cmd.deviceId} -> ${cmd.action}`,\n roomId: \"studio-main\",\n meta: { commandId: cmd.id, adapter: cmd.adapter }\n });\n sendJson(res, 200, { ok: true, command: cmd });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listCommands(auth.uid, 200);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/robotics/commands/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updateCommandStatus(auth.uid, id, payload?.status || \"sent\");\n sendJson(res, 200, { ok: true, command: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/rhizoh/bridge\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const result = await runRhizohBrainV2({\n message: String(payload?.message || \"robotics bridge command\"),\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n worldState: payload?.worldState || {},\n deviceState: payload?.deviceState || {},\n intent: \"robotics-mechanics\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-bridge\",\n eventType: \"robotics-command\",\n text: `Bridge -> ${String(result.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { directive: result.directive, agentId: payload?.agentId || \"\" }\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"robotics_bridge_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/planner/graph\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const graph = buildTaskActionGraph(String(payload?.task || \"\"), payload?.context || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"planner-graph\",\n text: `Planner graph built: ${graph.taskTitle} nodes=${graph.nodes.length}`,\n roomId: \"studio-main\",\n meta: { taskTitle: graph.taskTitle, nodeCount: graph.nodes.length }\n });\n sendJson(res, 200, { ok: true, graph });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"planner_graph_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerSocialChannel(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, channel: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_channel_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listSocialChannels(auth.uid, 100);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/broadcast\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const text = String(payload?.text || \"\").slice(0, 1200);\n const channels = await listSocialChannels(auth.uid, 100);\n const enabled = channels.filter((c) => c.status === \"enabled\");\n const deliveries = [];\n for (const c of enabled) {\n const out = await deliverSocialWithRetry(c, text);\n deliveries.push({ id: c.id, platform: c.platform, ...out });\n }\n const okCount = deliveries.filter((d) => d.ok).length;\n await addTranscript(auth.uid, {\n source: \"social-layer\",\n eventType: \"social-broadcast\",\n text: `Broadcast delivered ${okCount}/${enabled.length} channel(s): ${text.slice(0, 240)}`,\n roomId: \"studio-main\",\n meta: { deliveries }\n });\n sendJson(res, 200, { ok: true, sentChannels: okCount, totalChannels: enabled.length, deliveries });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_broadcast_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/ethics/programs\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, ...buildEthicsPrograms() });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands/dispatch\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const all = await listCommands(auth.uid, 200);\n const queued = all.filter((c) => String(c.status || \"queued\") === \"queued\").slice(0, 50);\n const devices = await listDevices(auth.uid, 500);\n const byId = new Map(devices.map((d) => [d.id, d]));\n const results = [];\n for (const cmd of queued) {\n const dev = byId.get(cmd.deviceId);\n if (!dev) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: \"device_not_found\" });\n continue;\n }\n try {\n const dispatched = await dispatchCommandToAdapter(dev, cmd);\n await updateCommandStatus(auth.uid, cmd.id, \"sent\");\n results.push({ id: cmd.id, ok: true, adapter: dispatched.adapter, via: dispatched.via });\n } catch (error) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: String(error?.message || \"dispatch_failed\") });\n }\n }\n await addTranscript(auth.uid, {\n source: \"robotics-command-dispatch\",\n eventType: \"command-dispatch-batch\",\n text: `Dispatch batch size=${results.length} ok=${results.filter((x) => x.ok).length}`,\n roomId: \"studio-main\",\n meta: { results: results.slice(0, 30) }\n });\n sendJson(res, 200, { ok: true, processed: results.length, results });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"dispatch_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/pdf/jobs/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n const job = await getPdfJob(auth.uid, id);\n if (!job) return sendJson(res, 404, { ok: false, error: \"pdf_job_not_found\" });\n sendJson(res, 200, { ok: true, job });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const row = await appendMemory({\n scope,\n id,\n text: payload?.text,\n tags: payload?.tags || [],\n importance: payload?.importance,\n kind: payload?.kind || \"episodic\",\n meta: { uid: auth.uid, ...(payload?.meta || {}) }\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.externalTruth) {\n try {\n if (process.env.RHIZOH_EXTERNAL_TRUTH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_truth:${ip}`, RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n sendJson(res, 200, buildRhizohExternalGroundTruthPayload(req));\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.productOutcome) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const rawBody = await readHttpBodyText(req, 64 * 1024);\n const body = rawBody.trim() ? safeJsonParse(rawBody) : null;\n if (!body || typeof body !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"invalid_json_payload\" });\n return;\n }\n const sigHdr = String(req.headers?.[\"x-rhizoh-outcome-signature\"] || \"\").trim();\n const hmacV = verifyRhizohOutcomeHmac(rawBody, sigHdr);\n if (!hmacV.ok) {\n sendJson(res, 401, { ok: false, error: hmacV.error || \"outcome_hmac_invalid\" });\n return;\n }\n const srcTokHdr = String(req.headers?.[\"x-rhizoh-outcome-source-token\"] || \"\").trim();\n const srcV = verifyRhizohOutcomeSourceToken(body.source, srcTokHdr);\n if (!srcV.ok) {\n sendJson(res, 401, { ok: false, error: srcV.error || \"outcome_source_token_invalid\" });\n return;\n }\n const subjectKey = String(body.subjectKey || \"\").trim().slice(0, 128);\n if (\n subjectKey &&\n !checkHttpRateLimit(\n `rhizoh_product_outcome_subj:${subjectKey}`,\n RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN,\n 60_000\n )\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_subject\" });\n return;\n }\n const result = ingestRhizohProductOutcomeHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n schemaVersion: \"1.1.0\",\n aggregate: result.aggregate,\n integrity: result.integrity\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"outcome_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.productOutcomeAggregate) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome_agg:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const u = new URL(String(req.url || \"/\"), \"http://localhost\");\n const cohortId = u.searchParams.get(\"cohortId\") || \"\";\n const decisionFingerprint = u.searchParams.get(\"decisionFingerprint\") || \"\";\n const limit = u.searchParams.get(\"limit\") || \"50\";\n const rows = listRhizohProductOutcomeAggregates({\n cohortId: cohortId || undefined,\n decisionFingerprint: decisionFingerprint || undefined,\n limit: Number(limit) || 50\n });\n sendJson(res, 200, { ok: true, schemaVersion: \"1.1.0\", rows });\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.externalLossBatch) {\n try {\n if (process.env.RHIZOH_EXTERNAL_LOSS_BATCH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_loss_batch:${ip}`, RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const result = ingestRhizohExternalLossBatchHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, { ok: true, accepted: result.accepted ?? 0 });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"batch_failed\") });\n }\n return;\n }\n\n function moderationAdminOk(req) {\n const expected = String(process.env.CASTLE_MODERATION_ADMIN_KEY || \"\").trim();\n if (!expected) return false;\n const got = String(req.headers[\"x-castle-moderation-key\"] || \"\").trim();\n return got.length > 0 && got === expected;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.opsAbuseReport) {\n try {\n const body = await readHttpJson(req, 32 * 1024);\n const auth = await resolveHttpUser(req);\n const entry = submitAbuseReportV0({\n reporterUid: auth.ok ? auth.uid : null,\n targetUid: body?.targetUid ?? null,\n category: body?.category,\n detail: body?.detail,\n traceId: body?.traceId ?? null\n });\n sendJson(res, 202, { ok: true, reportId: entry.id, status: entry.status });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsModerationQueue) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n queue: listModerationQueueV0(100),\n softBlocked: listSoftBlockedUsersV0()\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-block\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const out = softBlockUserV0(body?.uid, body?.reason);\n sendJson(res, out.ok ? 200 : 400, out);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-unblock\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n sendJson(res, 200, removeSoftBlockV0(body?.uid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsAgentSnapshots) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n const limit = Number(new URL(req.url || \"\", \"http://local\").searchParams.get(\"limit\") || 50);\n sendJson(res, 200, { ok: true, snapshots: listRecentAgentSnapshotsV0(limit) });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.academicObservatoryExport) {\n const auth = await resolveHttpUser(req);\n const out = handleAcademicObservatoryExportGetV0(req, auth);\n sendJson(res, out.status, out.body);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsHardeningStatus) {\n const auth = await resolveHttpUser(req);\n const principal = auth.ok ? `uid:${auth.uid}` : null;\n const gclSnap = await getGlobalCostLedgerSnapshotV0(principal);\n const economicStrategy = evaluateEconomicStrategyL3V0(gclSnap);\n const hardeningTenantId = resolveHardeningTenantIdV0(auth, req);\n const unifiedState = await buildUnifiedStateNarrativeV0({\n tenantId: hardeningTenantId ?? undefined,\n platformScope: !hardeningTenantId,\n principal\n });\n sendJson(res, 200, {\n ok: true,\n schema: \"rhizoh.ops.hardening_status.v0\",\n containment: readAgentContainmentConfigV0(),\n cost: readCostContainmentConfigV0(),\n gcl: {\n backend: await getGclBackendStatusV0(),\n snapshot: gclSnap\n },\n economicStrategyL3: economicStrategy,\n phasedRollout: await getPhasedRolloutStatsV0(),\n tier: readPhasedRolloutTierV0(),\n principalCost: auth.ok ? await getCostContainmentStatsV0(principal) : null,\n stressTaxonomy: verifyStressTaxonomyCoverageV0(),\n stressConflictResolution: verifyStressConflictResolutionV0(),\n resolutionStabilityEnvelope: verifyResolutionStabilityEnvelopeV0(),\n unifiedState: {\n tenantScope: {\n tenantId: unifiedState.tenantScope?.tenantId,\n scope: unifiedState.tenantScope?.scope,\n noGlobalDerivedState: unifiedState.tenantScope?.noGlobalDerivedState\n },\n narrativeFingerprint: unifiedState.narrativeFingerprint,\n screenshotScopeWatermark: unifiedState.screenshotScopeWatermark,\n interpretationSafetyContract: unifiedState.interpretationSafetyContract,\n interpretationUxContract: {\n schema: unifiedState.interpretationUxContract?.schema,\n layerOrder: unifiedState.interpretationUxContract?.layerOrder,\n narrativeNeverVisuallyDominant: unifiedState.interpretationUxContract?.narrativeNeverVisuallyDominant\n },\n governance: {\n narrativeIsNotDecisionLayer: unifiedState.governance.narrativeIsNotDecisionLayer,\n displayOrder: unifiedState.governance.displayOrder,\n trustPosture: unifiedState.governance.trustPosture,\n interpretationUxContract: unifiedState.governance.interpretationUxContract?.schema\n },\n humanOps: {\n decisionTier: unifiedState.humanOps?.humanDecisionScaling?.tier,\n decisionOwner: unifiedState.humanOps?.humanDecisionScaling?.decisionOwner,\n opsRouteId: unifiedState.humanOps?.humanDecisionOpsRunbook?.routing?.routeId,\n slaAckMinutes: unifiedState.humanOps?.humanDecisionOpsRunbook?.sla?.ackMinutes,\n uiDisclaimerRequired: unifiedState.humanOps?.narrativeUiSafety?.displayRules?.requireDisclaimerBanner,\n misreadHighResidual: unifiedState.humanOps?.misreadSimulation?.highResidualCount,\n propagationHighResidual: unifiedState.humanOps?.socialPropagationSimulation?.highResidualCount,\n worstWatermarkSurvivability:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.worstWatermarkSurvivability,\n dominantDistortionSource:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.dominantDistortionSource\n },\n culturalRisk: {\n perceptualRiskClass: unifiedState.culturalRisk?.perceptualRiskClass,\n trustFork: unifiedState.culturalRisk?.trustDynamics?.fork,\n trustDecayScore: unifiedState.culturalRisk?.trustDynamics?.scores?.trustDecay,\n mythologyScore: unifiedState.culturalRisk?.trustDynamics?.scores?.mythology,\n confidenceDetachmentExposure:\n unifiedState.culturalRisk?.trustDynamics?.axes?.confidenceDetachmentExposure\n },\n appliedSystemsLayer: {\n schema: unifiedState.appliedSystemsLayer?.schema,\n researchOutputType: unifiedState.appliedSystemsLayer?.researchGovernance?.outputType,\n roboticsBlockActuation: unifiedState.appliedSystemsLayer?.roboticsGrounding?.blockActuation,\n spiralRumorCount:\n unifiedState.appliedSystemsLayer?.spiralMMOGameKernel?.gameLayer?.rumors?.length\n },\n actionSemanticGovernance: {\n schema: unifiedState.actionSemanticGovernance?.schema,\n highRiskActionCount: unifiedState.actionSemanticGovernance?.highRiskActionCount,\n loopClosed: unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.loopClosed,\n authorityCollapseMitigated:\n unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.risks?.authorityCollapse\n ?.mitigated\n },\n actionContextResolution: {\n schema: unifiedState.actionContextResolution?.schema,\n contextFingerprint: unifiedState.actionContextResolution?.contextFingerprint?.fingerprintHash,\n domainCollision: unifiedState.actionContextResolution?.domainCollision?.detected,\n deployAgentSpiralBlocked:\n unifiedState.actionContextResolution?.actionOverloadingRisk?.deployAgentExample\n ?.spiral_mmo === \"blocked\",\n roboticsActuationEligible:\n unifiedState.actionContextResolution?.roboticsActuationCorrectness?.actuationEligible\n },\n epistemicCoherence: {\n schema: unifiedState.epistemicCoherence?.schema,\n verdict: unifiedState.epistemicCoherence?.systemCoherence?.verdict,\n coherenceScore: unifiedState.epistemicCoherence?.systemCoherence?.score,\n contradictionCount: unifiedState.epistemicCoherence?.crossLayerContradictions?.count,\n operatorDecision: unifiedState.epistemicCoherence?.uxCompression?.operatorDecision,\n headlineTr: unifiedState.epistemicCoherence?.uxCompression?.headline?.tr,\n falseSafetyWarning: Boolean(\n unifiedState.epistemicCoherence?.uxCompression?.falseSafetyWarning\n )\n },\n coherenceAuthorityBoundary: {\n schema: unifiedState.coherenceAuthorityBoundary?.schema,\n bindingValid: unifiedState.coherenceAuthorityBoundary?.eclAuthorityBinding?.valid,\n canExecute: unifiedState.coherenceAuthorityBoundary?.contract?.can_execute,\n mandatoryBannerTr:\n unifiedState.coherenceAuthorityBoundary?.disclaimers?.mandatoryBanner?.tr\n },\n decisionLatencyGovernance: {\n schema: unifiedState.decisionLatencyGovernance?.schema,\n latencyTier: unifiedState.decisionLatencyGovernance?.latencyTier,\n inflationScore: unifiedState.decisionLatencyGovernance?.latencyInflation?.score,\n fastPathEligible: unifiedState.decisionLatencyGovernance?.fastPath?.eligible,\n primaryActionId: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.primaryActionId,\n ackSlaMinutes: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.ackSlaMinutes,\n certaintyCap: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.certaintyCap,\n uncertaintyItems:\n unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.uncertaintyEnvelope?.itemCount\n },\n decisionPacketUncertaintyBoundary: {\n schema: unifiedState.decisionPacketUncertaintyBoundary?.schema,\n invariantValid: unifiedState.decisionPacketUncertaintyBoundary?.invariantCheck?.valid,\n centralInvariant: unifiedState.decisionPacketUncertaintyBoundary?.centralInvariant\n },\n epistemicDecisionPacing: {\n schema: unifiedState.epistemicDecisionPacing?.schema,\n queueSaturationTier:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation?.tier,\n operatorProcessingLatency:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation\n ?.operatorProcessingLatency,\n postureWindowCount:\n unifiedState.epistemicDecisionPacing?.temporaryStaticPostureWindows\n ?.temporaryStaticPostureWindows?.windowCount,\n executionBoundariesValid:\n unifiedState.epistemicDecisionPacing?.executionBoundaries?.valid,\n grantsExecutionApproval: unifiedState.epistemicDecisionPacing?.grantsExecutionApproval\n },\n epistemicTemporalCoherence: {\n schema: unifiedState.epistemicTemporalCoherence?.schema,\n allInvariantsValid: unifiedState.epistemicTemporalCoherence?.allInvariantsValid,\n temporalAlignmentValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.temporalAlignment?.valid,\n crossWindowGuardValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard?.valid,\n delayedTruthValid: unifiedState.epistemicTemporalCoherence?.invariants?.delayedTruthRule?.valid,\n reconciliationRequired:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard\n ?.reconciliation?.required\n },\n realityDriftObserver: {\n schema: unifiedState.realityDriftObserver?.schema,\n driftActive: unifiedState.realityDriftObserver?.driftObserverSummary?.active,\n misapprehensionShapeCount:\n unifiedState.realityDriftObserver?.misapprehensionShapeCatalog?.shapeCount,\n criticalShapeCount:\n unifiedState.realityDriftObserver?.driftObserverSummary?.criticalShapeCount,\n roboticsMismatchActive:\n unifiedState.realityDriftObserver?.roboticsFeedbackMismatch?.active\n },\n driftCausality: {\n schema: unifiedState.driftCausality?.schema,\n shapesWithCausalExplanation: unifiedState.driftCausality?.rdolLinkage?.shapesWithCausalExplanation,\n activeDomainCount: unifiedState.driftCausality?.causalitySummary?.activeDomainCount,\n propagationCausalityActive: unifiedState.driftCausality?.causalDomains?.propagation?.active\n },\n productReadiness: unifiedState.productReadiness,\n stateLayers: {\n raw: unifiedState.stateLayers.raw,\n derived: {\n systemState: unifiedState.stateLayers.derived.systemState,\n narrative: unifiedState.stateLayers.derived.narrative,\n validation: unifiedState.stateLayers.derived.validation\n },\n policy: {\n binding: unifiedState.stateLayers.policy.binding,\n decisionOwner: unifiedState.stateLayers.policy.decisionOwner,\n prohibitedActions: unifiedState.stateLayers.policy.prohibitedActions\n }\n },\n systemState: unifiedState.systemState,\n interpretation: unifiedState.interpretation,\n validation: {\n trustPosture: unifiedState.validation.trustPosture,\n adjustedConfidence: unifiedState.validation.adjustedConfidence,\n agreement: unifiedState.validation.confidenceDecomposition.agreement,\n divergenceScore: unifiedState.validation.divergence.divergenceScore,\n validated: unifiedState.validation.divergence.validated,\n uncertaintyTags: unifiedState.validation.uncertainty.tags.map((t) => t.id),\n flags: unifiedState.validation.divergence.flags.map((f) => f.id)\n },\n compression: unifiedState.compression\n }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 6 * 1024 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (\n !checkHttpRateLimit(`rhizoh_voice_transcribe:${rlKey}`, RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN, 60_000)\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const voiceEnv = rhizohVoiceTranscribeEnvV3();\n if (!voiceEnv.any) {\n sendJson(res, 503, {\n ok: false,\n error: \"voice_asr_not_configured\",\n env: voiceEnv\n });\n return;\n }\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n const result = await runRhizohVoiceTranscribeV3(\n applyLanguagePropagationToVoiceBodyV1(payload, langProp)\n );\n sendJson(res, result.ok ? 200 : 422, {\n ...result,\n traceId: String(payload?.traceId || \"\"),\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n atMs: Date.now()\n });\n } catch (error) {\n sendJson(res, 500, {\n ok: false,\n error: \"voice_transcribe_failed\",\n detail: String(error?.message || error),\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(\"/rhizoh/llm/task/\")) {\n try {\n const rawTaskId = decodeURIComponent(pathname.slice(\"/rhizoh/llm/task/\".length).split(\"/\")[0] || \"\");\n const taskId = String(rawTaskId || \"\").toLowerCase() === \"last\" ? getLatestLlmWorkerTaskIdV0() : rawTaskId;\n const snap = taskId ? getLlmWorkerTaskSnapshotV0(taskId) : null;\n if (!snap) {\n sendJson(res, 404, {\n ok: false,\n error: \"llm_task_not_found\",\n taskId: rawTaskId,\n status: \"missing\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n if (snap.status === \"processing\") {\n sendJson(res, 202, {\n ok: true,\n taskId: snap.taskId,\n status: \"processing\",\n createdAt: snap.createdAt,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n // Terminal states (completed/failed) always HTTP 200 — poll must not look like server fault (500).\n sendJson(res, 200, {\n ...(snap.body || { ok: false, error: \"llm_task_empty\", taskId: snap.taskId }),\n taskId: snap.taskId,\n status: snap.status === \"completed\" ? \"completed\" : snap.status,\n terminal: true,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n } catch (error) {\n console.error(\"[llm-task-poll] handler failed:\", error);\n sendJson(res, 200, {\n ok: false,\n error: \"llm_task_poll_handler_failed\",\n detail: String(error?.message || error),\n status: \"failed\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === rhizohRuntime.routes.rhizohLlm) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 128 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n logRhizohHealth(\"gateway_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: String(payload?.traceId || \"\"),\n auth: auth.ok ? \"ok\" : \"anon\",\n ip\n });\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (!checkHttpRateLimit(`rhizoh_llm:${rlKey}`, RL_RHIZOH_LLM_PER_MIN, 60_000)) {\n const rlTaxonomy = classifyStressResponseV0({ code: \"rate_limit_exceeded\" });\n return sendJson(res, 429, {\n ok: false,\n error: \"rate_limit_exceeded\",\n reply: rlTaxonomy.userMessageTr || \"İstek sınırı aşıldı. Kısa süre sonra tekrar deneyin.\",\n stressClass: rlTaxonomy.stressClass,\n responseAction: rlTaxonomy.responseAction,\n stressMatrix: rlTaxonomy.matrix,\n stressInterpretable: rlTaxonomy.interpretable,\n stressConfidence: rlTaxonomy.stressConfidence,\n actionConfidence: rlTaxonomy.actionConfidence,\n actionSoftened: rlTaxonomy.actionSoftened,\n responseActionStrict: rlTaxonomy.responseActionStrict,\n actionInterpretable: rlTaxonomy.actionInterpretable,\n conflictResolution: rlTaxonomy.conflictResolution,\n directive: \"NONE\"\n });\n }\n\n const requireExplicit = process.env.CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE === \"1\";\n const keyMode = normalizeClientLlmKeySource(payload?.llmKeySource ?? payload?.keySource);\n if (keyMode === null) {\n return sendJson(res, 400, {\n ok: false,\n error: \"invalid_llm_key_source\",\n reply: \"llmKeySource geçersiz. Kullanın: env | user_connection | auto\",\n directive: \"NONE\"\n });\n }\n if (requireExplicit && keyMode === \"auto\") {\n return sendJson(res, 400, {\n ok: false,\n error: \"llm_key_source_required\",\n reply: \"Sunucu CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE=1: istekte llmKeySource (env veya user_connection) zorunlu.\",\n directive: \"NONE\"\n });\n }\n\n const conn = auth.ok ? await resolveConnection(auth.uid, String(payload?.connectionId || \"\")) : null;\n\n if (keyMode === \"user_connection\") {\n if (!auth.ok) {\n return sendJson(res, 401, {\n ok: false,\n error: \"llm_auth_required_for_user_connection\",\n reply: \"Kayıtlı LLM bağlantısı için giriş gerekli.\",\n directive: \"NONE\"\n });\n }\n if (!conn?.apiKey) {\n return sendJson(res, 400, {\n ok: false,\n error: \"user_llm_connection_required\",\n reply: \"Geçerli bir LLM bağlantısı yok; Studio’da bağlantı ekleyin veya llmKeySource=env kullanın.\",\n directive: \"NONE\"\n });\n }\n }\n\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n let safePayload = applyLanguagePropagationToLlmPayloadV1({ ...(payload || {}) }, langProp);\n const trimOut = trimRhizohLlmGatewayPayloadV1(safePayload);\n if (trimOut.meta.trimmed) {\n logRhizohHealth(\"llm_payload_trimmed\", {\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n bytesBefore: trimOut.meta.bytesBefore,\n bytesAfter: trimOut.meta.bytesAfter\n });\n }\n safePayload = trimOut.payload;\n delete safePayload.apiKey;\n delete safePayload.llmKeySource;\n delete safePayload.keySource;\n\n const llmMessage = String(\n safePayload?.message || safePayload?.text || payload?.message || payload?.text || \"\"\n ).trim();\n if (!llmMessage) {\n return sendJson(res, 400, {\n ok: false,\n error: \"message_required\",\n reply: \"Mesaj metni gerekli (message alanı boş).\",\n directive: \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n if (!safePayload.message) {\n safePayload = { ...safePayload, message: llmMessage };\n }\n\n let resolvedProvider = payload?.provider;\n let resolvedModel = payload?.model;\n let connApiKey = \"\";\n\n ({\n resolvedProvider,\n resolvedModel,\n connApiKey\n } = resolveServerLlmProviderV0(keyMode, conn, payload));\n\n const turnInput = {\n safePayload,\n auth,\n keyMode,\n conn,\n resolvedProvider,\n resolvedModel,\n connApiKey\n };\n const turnMeta = {\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n langEcho: buildRhizohLanguagePropagationEchoV1(langProp)\n };\n\n if (\n isLlmWorkerEnabledV0() &&\n isLlmWorkerAsyncEnabledV0() &&\n String(req.headers[\"x-rhizoh-llm-sync\"] || \"\").trim() !== \"1\"\n ) {\n const taskId = enqueueLlmWorkerTaskAsyncV0(turnInput, turnMeta);\n logRhizohHealth(\"llm_worker_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n taskId,\n traceId: String(payload?.traceId || taskId),\n keyMode,\n async: true\n });\n sendJson(res, 202, {\n ok: true,\n taskId,\n status: \"processing\",\n traceId: String(payload?.traceId || taskId),\n pollPath: `/rhizoh/llm/task/${taskId}`,\n gatewayPid: process.pid,\n ...turnMeta.langEcho\n });\n return;\n }\n\n const { result, traceId, turnLatencyMs, spinePhases, sampledTrace } = isLlmWorkerEnabledV0()\n ? await runLlmWorkerTaskV0(turnInput, turnMeta)\n : await rhizohGatewayTurn(turnInput);\n logRhizohHealth(\"llm_response\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId,\n turnLatencyMs: Number(turnLatencyMs || 0),\n keyMode\n });\n sendJson(res, 200, {\n ...result,\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n traceId,\n turnLatencyMs,\n ...(spinePhases ? { spinePhases } : {}),\n ...(sampledTrace !== undefined ? { sampledTrace } : {})\n });\n } catch (error) {\n const msg = String(error?.message || \"\");\n const code = error?.code || \"\";\n let status = 500;\n if (msg === \"payload_too_large\") status = 413;\n else if (msg === \"rate_limit_exceeded\") status = 429;\n else if (\n code === \"cost_hard_limit\" ||\n code === \"phased_rollout_capacity\" ||\n error?.containment === true ||\n String(code).startsWith(\"agent_\")\n ) {\n status = 429;\n } else if (code === \"user_soft_blocked\" || code === \"prompt_abuse_detected\") status = 403;\n else if (\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(code) ||\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(msg) ||\n msg.includes(\"missing_api_key_for_\")\n )\n status = 400;\n let rhizohFailureKind = \"provider_error\";\n if (msg === \"rate_limit_exceeded\" || status === 429) rhizohFailureKind = \"rate_limit\";\n else if (status === 403) rhizohFailureKind = \"policy_block\";\n else if (status === 400 || status === 413) rhizohFailureKind = \"client_config\";\n else if (msg.startsWith(\"provider_http_\")) {\n const st = Number(msg.replace(\"provider_http_\", \"\"));\n if (st === 408 || st === 504) rhizohFailureKind = \"timeout\";\n else rhizohFailureKind = \"provider_error\";\n } else if (/timeout|timed out|ETIMEDOUT/i.test(msg)) rhizohFailureKind = \"timeout\";\n\n const providerHttpMatch = msg.match(/^provider_http_(\\d{3})$/);\n const stressTaxonomy =\n error?.stressClass != null\n ? {\n stressClass: error.stressClass,\n responseAction: error.responseAction,\n stressMatrix: error.stressMatrix,\n stressInterpretable: error.stressInterpretable !== false,\n stressConfidence: error.stressConfidence,\n actionConfidence: error.actionConfidence,\n actionSoftened: error.actionSoftened,\n responseActionStrict: error.responseActionStrict,\n actionInterpretable: error.actionInterpretable,\n conflictResolution: error.conflictResolution,\n stressSecondary: error.stressSecondary,\n responseActions: error.responseActions\n }\n : (() => {\n const t = classifyStressResponseV0({\n code: code || msg,\n rhizohFailureKind,\n providerHttpStatus: providerHttpMatch\n ? Number(providerHttpMatch[1])\n : null\n });\n return {\n stressClass: t.stressClass,\n responseAction: t.responseAction,\n stressMatrix: t.matrix,\n stressInterpretable: t.interpretable,\n stressConfidence: t.stressConfidence,\n actionConfidence: t.actionConfidence,\n actionSoftened: t.actionSoftened,\n responseActionStrict: t.responseActionStrict,\n actionInterpretable: t.actionInterpretable,\n conflictResolution: t.conflictResolution,\n stressSecondary: t.stressSecondary,\n responseActions: t.responseActions\n };\n })();\n sendJson(res, status, {\n ok: false,\n rhizohFailureKind,\n ...(providerHttpMatch ? { providerHttpStatus: Number(providerHttpMatch[1]) } : {}),\n error: code || msg || \"rhizoh_llm_failed\",\n detail: msg || code || undefined,\n reply:\n error?.reply ||\n (status === 500 ? \"Rhizoh bağlantısı geçici olarak kesildi.\" : msg),\n directive: error?.directive || \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n ...stressTaxonomy\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicSeal) {\n try {\n if (!EPISTEMIC_SEAL_SECRET) {\n sendJson(res, 503, { ok: false, error: \"epistemic_seal_disabled\" });\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`epistemic_seal:${ip}`, RL_EPISTEMIC_SEAL_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const truth = body.truth_contract;\n if (!truth || typeof truth !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"truth_contract_required\" });\n return;\n }\n const sealBody = {\n truth_contract: truth,\n runtime_hash: String(body.runtime_hash || \"\"),\n model_route: {\n provider: body.model_route?.provider ?? null,\n model: body.model_route?.model ?? null\n },\n memory_digest: String(body.memory_digest || \"\"),\n world_snapshot_hash: String(body.world_snapshot_hash || \"\"),\n timestamp: Number(body.timestamp) || Date.now()\n };\n const canonical = canonicalEpistemicSealString(sealBody);\n const { hash, signature } = hashAndSignEpistemicSeal(canonical, EPISTEMIC_SEAL_SECRET);\n recordGenesisEpistemicSealIssued(hash);\n const attestation = {\n schema: EPISTEMIC_SEAL_SCHEMA,\n algorithm: \"SHA-256+HMAC-SHA256\",\n keyHint: process.env.CASTLE_EPISTEMIC_SEAL_SECRET ? \"CASTLE_EPISTEMIC_SEAL_SECRET\" : \"CASTLE_GATEWAY_TOKEN\",\n issuedAt: Date.now(),\n gateway: { service: \"castle-gateway\", listenPort: PORT }\n };\n sendJson(res, 200, {\n ok: true,\n hash,\n signature,\n attestation,\n canonicalBytes: Buffer.byteLength(canonical, \"utf8\")\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"seal_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicLogsBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`epistemic_logs:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 120) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const persisted = await persistEpistemicLedgerBatch(auth.uid, entries);\n recordGenesisEpistemicLedgerPersisted(persisted.persisted);\n // Internal cognition layer: forecast stays gateway/ledger-only (no client exposure).\n try {\n await persistEpistemicForecastBatch(auth.uid, persisted.normalized || []);\n } catch {\n /* forecast failures must not break primary ledger ingest */\n }\n sendJson(res, 200, {\n ok: true,\n persisted: persisted.persisted,\n mode: persisted.mode,\n latest: persisted.normalized?.[persisted.normalized.length - 1] || null\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"epistemic_log_ingest_failed\") });\n }\n return;\n }\n\n sendJson(res, 404, { ok: false, error: \"not_found\" });\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\nspiralWssForGenesis = wss;\nconst orchestrator = createOrchestrator();\nconst clientStats = new Map();\nlet broadcasterClientId = null;\nconst spiralState = {\n rooms: [{ id: \"room-default\", name: \"Spiral Default Arena\" }],\n activeByClient: new Map(),\n characters: new Map() // id -> {id,name,role,roomId,source}\n};\n\n/** Castle social rooms — keyed by `castleRoomKey` from {@link WS_MESSAGE.CASTLE_SOCIAL_PULSE}. */\nconst castleSocialRoomState = new Map();\nconst CASTLE_SOCIAL_MEMBER_TTL_MS = 90_000;\nconst CASTLE_SOCIAL_MAX_PULSE_BYTES = 8192;\nconst RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES = 5 * 1024 * 1024;\nconst RHIZOH_VOICE_LIVE_MAX_CHUNKS = 96;\n\n/**\n * Voice Engine v3 live media lane.\n * Client streams MediaRecorder chunks over the existing gateway WS; STOP reuses the\n * gateway ASR runner so orchestration remains the single authority downstream.\n */\nconst rhizohVoiceLiveSessions = new WeakMap();\n\nfunction appendRhizohVoiceLiveEventV0(session, type, payloadRefSeed = \"\") {\n if (!session) return null;\n session.eventSeq = (Number(session.eventSeq) || 0) + 1;\n const event = createRhizohImmutableEventV0({\n type,\n sessionId: session.sessionId,\n traceId: session.traceId,\n actorSeed: session.sessionId || session.traceId,\n eventSeq: session.eventSeq,\n payloadRef: createRhizohPayloadRefV0(payloadRefSeed || `${session.sessionId}:${type}:${session.eventSeq}`)\n });\n session.events = [...(session.events || []), event].slice(-64);\n return event;\n}\n\nfunction sendRhizohVoiceLiveEnvelopeV0(socket, type, payload) {\n try {\n if (socket?.readyState === 1) socket.send(JSON.stringify(createEnvelope(type, payload)));\n } catch {\n /* noop */\n }\n}\n\nfunction decodeRhizohVoiceLiveChunkV0(payload = {}) {\n const b64 = String(payload.audioBase64 || \"\").trim();\n if (!b64) return { ok: false, error: \"audio_base64_required\" };\n try {\n const buf = Buffer.from(b64, \"base64\");\n if (!buf.length) return { ok: false, error: \"audio_decode_empty\" };\n return { ok: true, buffer: buf };\n } catch {\n return { ok: false, error: \"audio_base64_invalid\" };\n }\n}\n\nfunction handleRhizohVoiceLiveStartV0(socket, payload = {}) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const sessionId = String(p.sessionId || `live_${Date.now().toString(36)}`).slice(0, 96);\n rhizohVoiceLiveSessions.set(socket, {\n sessionId,\n traceId: String(p.traceId || \"\").slice(0, 128),\n mimeType: String(p.mimeType || \"audio/webm\").slice(0, 80),\n languageCode: String(p.languageCode || \"tr-TR\").slice(0, 32),\n path: String(p.path || \"both\").slice(0, 24),\n chunks: [],\n chunkIndexes: [],\n totalBytes: 0,\n nextChunkIndex: 1,\n eventSeq: 0,\n events: [],\n startedAtMs: Date.now()\n });\n appendRhizohVoiceLiveEventV0(rhizohVoiceLiveSessions.get(socket), \"VOICE_LIVE_SESSION_STARTED\", `${sessionId}:live_session`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_START, {\n ok: true,\n sessionId,\n schema: \"castle.rhizoh.voice_live.gateway_lane.v0\"\n });\n}\n\nfunction handleRhizohVoiceLiveChunkV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const sequence = validateRhizohVoiceLiveChunkPayloadV0(payload, {\n expectedChunkIndex: session.nextChunkIndex\n });\n if (!sequence.ok) {\n rhizohVoiceLiveSessions.delete(socket);\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:chunk_reject`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: sequence.error,\n expectedChunkIndex: sequence.expectedChunkIndex,\n chunkIndex: sequence.chunkIndex\n });\n return;\n }\n const decoded = decodeRhizohVoiceLiveChunkV0(payload);\n if (!decoded.ok) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: decoded.error\n });\n return;\n }\n if (\n session.chunks.length >= RHIZOH_VOICE_LIVE_MAX_CHUNKS ||\n session.totalBytes + decoded.buffer.length > RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES\n ) {\n rhizohVoiceLiveSessions.delete(socket);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_audio_too_large\"\n });\n return;\n }\n session.chunks.push(decoded.buffer);\n session.chunkIndexes.push(sequence.chunkIndex);\n session.totalBytes += decoded.buffer.length;\n session.nextChunkIndex += 1;\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_CHUNK_ACCEPTED\", `${session.sessionId}:chunk:${sequence.chunkIndex}`);\n}\n\nfunction handleRhizohVoiceLiveStopV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n rhizohVoiceLiveSessions.delete(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const lastChunkIndex = Number(payload?.lastChunkIndex);\n if (Number.isInteger(lastChunkIndex) && lastChunkIndex !== session.chunks.length) {\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:stop_mismatch`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_chunk_count_mismatch\",\n expectedChunkCount: lastChunkIndex,\n receivedChunkCount: session.chunks.length\n });\n return;\n }\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_STOP_RECEIVED\", `${session.sessionId}:stop:${session.chunks.length}`);\n const audio = Buffer.concat(session.chunks);\n if (!audio.length) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"audio_empty\"\n });\n return;\n }\n runRhizohVoiceTranscribeV3({\n path: String(payload?.path || session.path || \"both\"),\n audioBase64: audio.toString(\"base64\"),\n mimeType: String(payload?.mimeType || session.mimeType || \"audio/webm\"),\n languageCode: String(payload?.languageCode || session.languageCode || \"tr-TR\"),\n traceId: String(payload?.traceId || session.traceId || \"\"),\n sessionId: session.sessionId,\n transport: \"gateway_ws_live\"\n })\n .then((result) => {\n appendRhizohVoiceLiveEventV0(session, result?.ok ? \"VOICE_TRANSCRIBE_FINAL\" : \"VOICE_TRANSCRIBE_FAILED\", `${session.sessionId}:final`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_FINAL, {\n ...result,\n sessionId: session.sessionId,\n transportPath: \"gateway_ws_live\",\n bytes: audio.length,\n chunkCount: session.chunks.length,\n chunkSequence: Object.freeze({\n ok: true,\n firstChunkIndex: session.chunkIndexes[0] || null,\n lastChunkIndex: session.chunkIndexes[session.chunkIndexes.length - 1] || null,\n receivedChunkCount: session.chunks.length\n }),\n eventTrace: Object.freeze((session.events || []).slice(-16)),\n recordedMs: Math.max(0, Date.now() - session.startedAtMs)\n });\n })\n .catch((e) => {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_transcribe_failed\",\n detail: String(e?.message || e)\n });\n });\n}\n\nfunction removeCastleSocialClientFromAllRooms(clientId) {\n const cid = String(clientId || \"\");\n if (!cid) return;\n for (const room of castleSocialRoomState.values()) {\n room.members.delete(cid);\n }\n}\n\nfunction buildCastleSocialRoomPayload(roomKey) {\n const room = castleSocialRoomState.get(roomKey);\n if (!room) return null;\n const now = Date.now();\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const roster = [...room.members.values()].map((m) => ({\n userId: m.userId,\n lastMs: m.lastMs,\n ...m.sp\n }));\n const peerFeeds =\n room.walPeers && room.walPeers.size\n ? [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }))\n : [];\n return { castleRoomKey: roomKey, seq: room.seq, roster, peerFeeds, ts: now };\n}\n\nconst CASTLE_WAL_PEER_MAX_FEED_BYTES = 48 * 1024;\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {{ ok: boolean, code?: string, reason?: string }} gate\n */\nfunction rejectWalPeerIngressV0(socket, gate) {\n const code = gate.code || \"wal_peer_rejected\";\n recordWalPeerRejectV0(code);\n recordSubstrateOtelEventV0(\"wal_peer_reject\", { code, reason: String(gate.reason || \"\").slice(0, 120) });\n socket.send(\n JSON.stringify(\n createEnvelope(WS_MESSAGE.ERROR, {\n error: gate.reason || \"WAL peer ingress rejected.\",\n code\n })\n )\n );\n}\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {Record<string, unknown> | null | undefined} walPeerFeed\n * @returns {boolean}\n */\nfunction gateWalPeerIngressV0(socket, walPeerFeed) {\n const authGate = assertWalPeerSocketAuthorityV0(socket);\n if (!authGate.ok) {\n rejectWalPeerIngressV0(socket, authGate);\n return false;\n }\n const sigGate = validateWalPeerFeedSignatureV0(\n walPeerFeed && typeof walPeerFeed === \"object\" ? walPeerFeed : {}\n );\n if (!sigGate.ok) {\n rejectWalPeerIngressV0(socket, sigGate);\n return false;\n }\n return true;\n}\n\nfunction handleCastleWalPeerFeed(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_WAL_PEER_MAX_FEED_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_WAL_PEER_FEED too large.\" })));\n return;\n }\n const walPeerFeed = p.walPeerFeed && typeof p.walPeerFeed === \"object\" ? p.walPeerFeed : {};\n if (!gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const castleRoomKey = String(p.castleRoomKey || \"wal:convergence\").slice(0, 64);\n const castleId = String(p.castleId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId,\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: room.members.get(socket.clientId)?.sp ?? {}\n });\n const peerFeeds = [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }));\n const body = {\n castleRoomKey,\n seq: room.seq,\n peerFeeds,\n ts: now\n };\n const env = createEnvelope(WS_MESSAGE.CASTLE_WAL_PEER_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys(), ...room.walPeers.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction handleCastleSocialPulse(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_SOCIAL_MAX_PULSE_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_SOCIAL_PULSE too large.\" })));\n return;\n }\n const castleRoomKey = String(p.castleRoomKey || \"default\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n const sp = p.socialPulse && typeof p.socialPulse === \"object\" ? p.socialPulse : {};\n const walPeerFeed =\n p.walPeerFeed && typeof p.walPeerFeed === \"object\"\n ? p.walPeerFeed\n : sp.walPeerFeed && typeof sp.walPeerFeed === \"object\"\n ? sp.walPeerFeed\n : null;\n if (walPeerFeed && !gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const spNext = walPeerFeed ? { ...sp, walPeerFeed } : { ...sp };\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: spNext\n });\n if (walPeerFeed) {\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId: String(p.castleId || userId).slice(0, 64),\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n }\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const body = buildCastleSocialRoomPayload(castleRoomKey);\n if (!body) return;\n const env = createEnvelope(WS_MESSAGE.CASTLE_SOCIAL_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcast(msgObj) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcastState() {\n broadcast(\n createEnvelope(WS_MESSAGE.BROADCAST_STATE, {\n broadcasterClientId,\n viewers: [...wss.clients].filter((c) => c.readyState === 1).length\n })\n );\n}\n\nfunction currentSpiralStatePayload() {\n return {\n rooms: spiralState.rooms,\n characters: [...spiralState.characters.values()]\n };\n}\n\nfunction broadcastSpiralState() {\n broadcast(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload()));\n}\n\nfunction ensureRoom(roomId, fallbackName = null) {\n if (!roomId) return spiralState.rooms[0].id;\n const exists = spiralState.rooms.find((r) => r.id === roomId);\n if (exists) return exists.id;\n const created = { id: roomId, name: fallbackName || `Spiral-${roomId.slice(-4)}` };\n spiralState.rooms.push(created);\n return created.id;\n}\n\nfunction syncSpiralCharactersWithWorld(snapshot) {\n const agents = snapshot?.agents || [];\n for (const a of agents) {\n if (!spiralState.characters.has(a.id)) {\n spiralState.characters.set(a.id, {\n id: a.id,\n name: a.id,\n role: \"agent\",\n roomId: spiralState.rooms[0].id,\n source: \"world\"\n });\n }\n }\n}\n\nfunction broadcastExcept(msgObj, exceptSocket) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client !== exceptSocket && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction validateInputFrame(payload, socket) {\n const stats = clientStats.get(socket.clientId) || { lastFrame: 0, windowStart: Date.now(), msgCount: 0 };\n const now = Date.now();\n if (now - stats.windowStart > 5000) {\n stats.windowStart = now;\n stats.msgCount = 0;\n }\n stats.msgCount += 1;\n clientStats.set(socket.clientId, stats);\n if (stats.msgCount > 120) return \"Rate limit exceeded.\";\n\n if (!payload || !Number.isInteger(payload.frame)) return \"frame must be integer.\";\n if (payload.frame <= stats.lastFrame) return \"frame must be monotonic.\";\n const commands = Array.isArray(payload.commands) ? payload.commands : [];\n if (commands.length > 8) return \"Too many commands in one frame.\";\n for (const cmd of commands) {\n if (!cmd || typeof cmd.command !== \"string\") return \"Invalid command format.\";\n if (\n cmd.command !== \"SPAWN_AGENT\" &&\n cmd.command !== \"LIST_CASTLES\" &&\n cmd.command !== \"SPAWN_ENTITY\" &&\n cmd.command !== \"CREATE_CASTLE\"\n ) return \"Unsupported command.\";\n }\n stats.lastFrame = payload.frame;\n clientStats.set(socket.clientId, stats);\n return null;\n}\n\nfunction validateSignalPayload(payload) {\n if (!payload || typeof payload !== \"object\") return \"Invalid signaling payload.\";\n const signalType = payload.signalType;\n if (Object.values(CASTLE_NETWORK_SIGNAL_V0).includes(signalType)) {\n return null;\n }\n if (![\"OFFER\", \"ANSWER\", \"ICE\"].includes(signalType)) return \"Invalid signal type.\";\n if (typeof payload.to !== \"string\" || payload.to.length < 3) return \"Invalid signal destination.\";\n if (signalType !== \"ICE\" && JSON.stringify(payload.sdp || {}).length > 16 * 1024) return \"SDP too large.\";\n if (signalType === \"ICE\" && JSON.stringify(payload.candidate || {}).length > 6 * 1024) return \"ICE too large.\";\n return null;\n}\n\nfunction sanitizeRawMessage(raw) {\n if (!raw) return null;\n const text = raw.toString();\n if (Buffer.byteLength(text, \"utf8\") > MAX_MESSAGE_BYTES) return null;\n return text;\n}\n\nwss.on(\"connection\", async (socket, req) => {\n const origin = req?.headers?.origin || \"\";\n if (!httpCors.isWebSocketOriginAllowed(origin)) {\n socket.close(1008, \"Origin not allowed\");\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const path = req?.url || \"\";\n const query = new URL(`ws://localhost${path}`).searchParams;\n const token = query.get(\"token\");\n if (!token || token !== REQUIRED_GATEWAY_TOKEN) {\n socket.close(1008, \"Invalid gateway token\");\n return;\n }\n }\n if (REQUIRE_AUTH) {\n const result = await verifyClientToken(req);\n if (!result.ok) {\n const isLocal = origin.includes(\"localhost\") || origin.includes(\"127.0.0.1\") || !origin;\n if (!(ALLOW_DEV_ANON && isLocal)) {\n socket.close(1008, result.reason);\n return;\n }\n socket.auth = { ok: true, kind: \"dev-anon\", user: { uid: \"dev-anon\" } };\n } else {\n socket.auth = result;\n }\n }\n\n socket.clientId = `c-${Math.random().toString(36).slice(2, 8)}`;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.HELLO, { message: \"Castle Gateway online.\", clientId: socket.clientId })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.WORLD_SNAPSHOT, orchestrator.worldSnapshot())));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.BROADCAST_STATE, { broadcasterClientId, viewers: [...wss.clients].filter((c) => c.readyState === 1).length })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload())));\n broadcastExcept(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }), socket);\n\n socket.on(\"message\", (raw) => {\n const safeRaw = sanitizeRawMessage(raw);\n if (!safeRaw) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Message too large or invalid.\" })));\n return;\n }\n const parsed = safeJsonParse(safeRaw);\n if (!parsed?.type) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid message.\" })));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND_TEXT) {\n const canonical = parseTextToCommand(parsed.payload?.text);\n if (!canonical) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Unknown command text.\" })));\n return;\n }\n const result = orchestrator.applyCommand(canonical);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND) {\n const result = orchestrator.applyCommand(parsed.payload);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n }\n\n if (parsed.type === WS_MESSAGE.INPUT_FRAME) {\n const err = validateInputFrame(parsed.payload, socket);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const frame = parsed.payload.frame;\n const commands = Array.isArray(parsed.payload?.commands) ? parsed.payload.commands : [];\n orchestrator.queueInputFrame(socket.clientId, frame, commands);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, queued: true, frame })));\n }\n\n if (parsed.type === WS_MESSAGE.SIGNAL) {\n const payload = parsed.payload || {};\n if (Object.values(CASTLE_NETWORK_SIGNAL_V0).includes(payload.signalType)) {\n const netErr = validateCastleNetworkSignalPayloadV0(socket, payload);\n if (netErr) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: netErr })));\n return;\n }\n handleCastleNetworkSignalV0(socket, payload, wss);\n return;\n }\n const err = validateSignalPayload(payload);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const signalType = payload.signalType;\n const to = payload.to;\n for (const client of wss.clients) {\n if (client.clientId === to && client.readyState === 1) {\n client.send(JSON.stringify(createEnvelope(WS_MESSAGE.SIGNAL, { ...payload, from: socket.clientId })));\n break;\n }\n }\n }\n\n if (parsed.type === WS_MESSAGE.DRONE_TELEMETRY) {\n const payload = parsed.payload || {};\n const json = JSON.stringify(payload);\n if (Buffer.byteLength(json, \"utf8\") > 48 * 1024) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"DRONE_TELEMETRY payload too large.\" })));\n return;\n }\n broadcastExcept(createEnvelope(WS_MESSAGE.DRONE_TELEMETRY, { ...payload, relayFrom: socket.clientId }), socket);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_START) {\n handleRhizohVoiceLiveStartV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_CHUNK) {\n handleRhizohVoiceLiveChunkV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_STOP) {\n handleRhizohVoiceLiveStopV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_SOCIAL_PULSE) {\n handleCastleSocialPulse(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_WAL_PEER_FEED) {\n handleCastleWalPeerFeed(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.OPEN_DATA_QUERY) {\n queryOpenData(parsed.payload)\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: true, result })));\n })\n .catch((e) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: false, error: e.message })));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_QUERY) {\n runRhizohBrainV2({\n ...(parsed.payload || {}),\n uid: socket?.auth?.user?.uid || \"unknown\"\n })\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, result)));\n })\n .catch(async () => {\n const fallback = await runRhizohBrain(parsed.payload || {}).catch(() => ({\n ok: false,\n reply: \"Rhizoh temporary disturbance. Habitat remains stable.\"\n }));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, fallback)));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.BROADCAST_REGISTER) {\n const role = String(parsed.payload?.role || \"\");\n if (role === \"GENESIS_BROADCAST_AGENT\") {\n broadcasterClientId = socket.clientId;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, registered: role })));\n broadcastState();\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.STUDIO_CUE) {\n // Only registered broadcaster can push global cues.\n if (!broadcasterClientId || socket.clientId !== broadcasterClientId) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Only broadcaster agent can emit STUDIO_CUE.\" })));\n return;\n }\n const cue = parsed.payload || {};\n if (typeof cue !== \"object\") {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid STUDIO_CUE payload.\" })));\n return;\n }\n broadcast(createEnvelope(WS_MESSAGE.STUDIO_CUE, cue));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_JOIN_ROOM) {\n const requested = String(parsed.payload?.roomId || \"\").trim();\n const requestedName = String(parsed.payload?.roomName || \"\").trim();\n const roomId = ensureRoom(requested || `room-${Math.random().toString(36).slice(2, 7)}`, requestedName || null);\n spiralState.activeByClient.set(socket.clientId, roomId);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, roomId })));\n broadcastSpiralState();\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_CREATE_CHARACTER) {\n const name = String(parsed.payload?.name || \"\").trim().slice(0, 32) || `AI-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;\n const role = String(parsed.payload?.role || \"agent\").trim().slice(0, 24) || \"agent\";\n const roomId = ensureRoom(\n String(parsed.payload?.roomId || spiralState.activeByClient.get(socket.clientId) || spiralState.rooms[0].id),\n null\n );\n\n // Character creation always maps to an authoritative world agent.\n const result = orchestrator.applyCommand({\n command: COMMAND.SPAWN_AGENT,\n params: { targetCastleId: \"castle-01\" }\n });\n if (!result?.ok || !result?.data?.id) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Character creation failed.\" })));\n return;\n }\n const id = result.data.id;\n spiralState.characters.set(id, { id, name, role, roomId, source: \"spawned\" });\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, characterId: id, roomId })));\n broadcastSpiralState();\n return;\n }\n });\n\n socket.on(\"close\", () => {\n if (socket.clientId === broadcasterClientId) broadcasterClientId = null;\n clientStats.delete(socket.clientId);\n rhizohVoiceLiveSessions.delete(socket);\n spiralState.activeByClient.delete(socket.clientId);\n removeCastleSocialClientFromAllRooms(socket.clientId);\n removeCastleNetworkClientV0(socket.clientId, wss);\n broadcast(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }));\n broadcastState();\n });\n});\n\nsetInterval(() => {\n void (async () => {\n try {\n const { frame, snapshot, delta, packedDelta } = orchestrator.tick(1 / 20);\n syncSpiralCharactersWithWorld(snapshot);\n broadcast(createEnvelope(WS_MESSAGE.NETWORK_TICK, { frame }));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_TICK, snapshot));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA, delta));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA_PACKED, packedDelta));\n broadcastSpiralState();\n await runAcademyEventTick();\n const executed = await tickAutomations(Date.now());\n for (const a of executed) {\n await addTranscript(a.uid, {\n source: \"planner\",\n eventType: \"automation-executed\",\n text: `Automation executed: ${a.title} (${a.action})`,\n roomId: \"studio-main\",\n meta: { automationId: a.id, action: a.action, deviceId: a.deviceId || \"\" }\n });\n }\n } catch (e) {\n console.error(\"[GATEWAY] world tick / automations loop error:\", String(e?.message || e));\n }\n })();\n}, 50);\n\n(async () => {\n await rhizohRuntime.telemetry.initOpenTelemetry();\n if (String(process.env.CASTLE_GENESIS_DISK_PERSIST ?? \"\").trim() === \"\") {\n process.env.CASTLE_GENESIS_DISK_PERSIST = \"1\";\n }\n const genesisDiskBoot = await hydrateGenesisContinuityPersistenceBootV0();\n if (!genesisDiskBoot.ok) {\n console.warn(\"[GATEWAY] genesis disk hydrate failed; continuing with in-memory genesis continuity\");\n }\n httpServer.listen(PORT, () => {\n startGenesisCanonicalClock();\n installGenesisCheckpointSurfaceGetter(buildGenesisRuntimeSurfacePayloadLive);\n setGenesisContinuityAfterPublishHook((seq) => {\n noteGenesisCheckpointSeqCommitted(seq);\n noteGenesisContinuitySeqAcceptedV0(seq);\n });\n startGenesisContinuityInfraSampler(buildGenesisRuntimeSurfacePayloadLive, 2000);\n logProductionObservatorySurfaceGuardsV0();\n logGatewaySubstrateAuthorityGuardsV0();\n if (isLlmWorkerEnabledV0()) {\n try {\n const workerBoot = initLlmWorkerAtBootV0();\n console.log(\"[GATEWAY] llm worker boot:\", workerBoot);\n } catch (workerErr) {\n console.error(\"[GATEWAY] llm worker boot failed:\", workerErr);\n }\n }\n console.log(`[GATEWAY] ws/http://localhost:${PORT}`);\n console.log(\n `[GATEWAY] genesis continuity: GET ${rhizohRuntime.routes.genesisRuntime} | POST ${rhizohRuntime.routes.genesisIngress} | SSE ${rhizohRuntime.routes.genesisStream} | replay ${rhizohRuntime.routes.genesisReplay} | diff ${rhizohRuntime.routes.genesisReplayDiff} | equiv ${rhizohRuntime.routes.genesisReplayEquivalence} | analytics ${rhizohRuntime.routes.genesisReplayAnalytics} | evolution ${rhizohRuntime.routes.genesisReplayEvolution}`\n );\n if (String(process.env.CASTLE_GENESIS_OBSERVATORY_LIVE_V01 ?? \"\").trim() === \"1\") {\n console.log(\"GENESIS OBSERVATORY LIVE v0.1 — READ ONLY PHASE STARTED\");\n }\n });\n})().catch((e) => {\n console.error(\"[GATEWAY] fatal boot (before or during listen):\", e);\n process.exit(1);\n});\n"}, "7b873557fca0fbf31ad3b18fdda34f00aab6de77": {"t": 1785754136.1221478, "n": 172041, "h": "4b65e89a2778349d", "c": "import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport dotenv from \"dotenv\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconsole.log(\"🔥 [GENESIS_BOOT] entry file:\", __filename);\nconsole.log(\"🔥 [GENESIS_BOOT] node env:\", process.env.NODE_ENV);\nconsole.log(\"🔥 [GENESIS_BOOT] pid:\", process.pid);\nconsole.log(\"🧬 [GENESIS_BOOT] cwd:\", process.cwd());\nconsole.log(\"🧬 [GENESIS_BOOT] import.meta.url:\", import.meta.url);\ndotenv.config({ path: path.join(__dirname, \"..\", \".env\") });\ndotenv.config({ path: path.join(__dirname, \"..\", \".env.local\"), override: true });\n\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport { createServer } from \"node:http\";\nimport { createOrchestrator } from \"@castle/orchestrator\";\nimport { parseTextToCommand } from \"@castle/command-dsl\";\nimport {\n WS_MESSAGE,\n COMMAND,\n createEnvelope,\n createRhizohImmutableEventV0,\n createRhizohPayloadRefV0,\n safeJsonParse,\n validateRhizohVoiceLiveChunkPayloadV0\n} from \"@castle/protocol\";\nimport { queryOpenData } from \"./openData.js\";\nimport { verifyClientToken } from \"./auth.js\";\nimport { runRhizohBrain } from \"./rhizohBrain.js\";\nimport { queryRhizohLlm } from \"./rhizohLlmGateway.js\";\nimport { rhizohGatewayTurn } from \"./rhizohGatewayTurn.js\";\nimport {\n enqueueLlmWorkerTaskAsyncV0,\n getLatestLlmWorkerTaskIdV0,\n getLlmWorkerDebugSnapshotV0,\n getLlmWorkerTaskSnapshotV0,\n initLlmWorkerAtBootV0,\n isLlmWorkerAsyncEnabledV0,\n isLlmWorkerEnabledV0,\n runLlmWorkerTaskV0\n} from \"./llmWorkerRuntime.js\";\nimport {\n RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n runRhizohVoiceTranscribeV3,\n rhizohVoiceTranscribeEnvV3\n} from \"./rhizohVoiceTranscribeV3.js\";\nimport { createHttpCorsPolicy, normalizeHttpOrigin, parseAllowedOriginsFromEnv } from \"./httpCorsPolicyV1.js\";\nimport {\n applyLanguagePropagationToLlmPayloadV1,\n applyLanguagePropagationToVoiceBodyV1,\n buildRhizohLanguagePropagationEchoV1,\n parseRhizohLanguagePropagationV1\n} from \"./rhizohLanguagePropagationV1.js\";\nimport { trimRhizohLlmGatewayPayloadV1 } from \"./rhizohLlmPayloadTrimV1.js\";\nimport {\n meshAppendDelta,\n meshContinuityAggregate,\n meshJoin,\n meshLeave,\n meshReplayFromNodeId,\n meshReplayFromSeq,\n meshSnapshot,\n meshSubscribeSse\n} from \"./presenceRoomMesh.js\";\nimport { runRhizohBrainV2 } from \"./rhizohBrainV2.js\";\nimport {\n listConnections,\n createConnection,\n updateConnection,\n deleteConnection,\n setDefaultConnection,\n resolveConnection\n} from \"./llmConnectionsStore.js\";\nimport { getCloudSyncSnapshotV0, mergeCloudSyncSnapshotV0 } from \"./castleCloudSyncStore.js\";\nimport {\n CASTLE_NETWORK_SIGNAL_V0,\n handleCastleNetworkSignalV0,\n listCastleNetworkPresenceV0,\n removeCastleNetworkClientV0,\n validateCastleNetworkSignalPayloadV0\n} from \"./castleNetworkRelayV0.js\";\nimport { checkHttpRateLimit, getHttpClientIp } from \"./castleHttpRateLimit.js\";\nimport {\n submitAbuseReportV0,\n listModerationQueueV0,\n softBlockUserV0,\n removeSoftBlockV0,\n listSoftBlockedUsersV0\n} from \"./ops/moderationMvpV0.js\";\nimport { listRecentAgentSnapshotsV0 } from \"./ops/agentObservabilityV0.js\";\nimport { readAgentContainmentConfigV0 } from \"./ops/agentContainmentV0.js\";\nimport { readCostContainmentConfigV0, getCostContainmentStatsV0 } from \"./ops/costContainmentV0.js\";\nimport { getGlobalCostLedgerSnapshotV0, getGclBackendStatusV0 } from \"./ops/globalCostLedgerV0.js\";\nimport { evaluateEconomicStrategyL3V0 } from \"./ops/economicStrategyEngineL3V0.js\";\nimport { getPhasedRolloutStatsV0, readPhasedRolloutTierV0 } from \"./ops/phasedRolloutV0.js\";\nimport { buildUnifiedStateNarrativeV0 } from \"./ops/unifiedStateNarrativeV0.js\";\nimport { resolveHardeningTenantIdV0 } from \"./ops/narrativeTenantIsolationV0.js\";\nimport {\n classifyStressResponseV0,\n verifyStressTaxonomyCoverageV0,\n verifyStressConflictResolutionV0\n} from \"./ops/stressResponseTaxonomyV0.js\";\nimport { verifyResolutionStabilityEnvelopeV0 } from \"./ops/resolutionStabilityEnvelopeV0.js\";\nimport { logLlmAccess } from \"./castleLlmAudit.js\";\nimport { appendMemory, listMemories, getMemoryContext, getPersonaGoalMemory, setPersonaGoalMemory, autoCompactMemories } from \"./memoryStore.js\";\nimport { getFirebasePersistence } from \"./firebasePersistence.js\";\nimport { registerAgentIdentity, listAgentIdentities, getAgentIdentity, updateAgentIdentity } from \"./agentIdentityStore.js\";\nimport { queueAcademyEvent, listAcademyEvents, resolveAcademyEvent, runAcademyEventTick } from \"./academyEventEngine.js\";\nimport { enqueuePdfJob, getPdfJob } from \"./pdfPipeline.js\";\nimport {\n createPublishSession,\n listPublishSessions,\n updatePublishSession,\n addTranscript,\n listTranscripts,\n searchMetadata,\n registerDevice,\n listDevices,\n updateDevice,\n appendTelemetry,\n listTelemetry,\n createAutomation,\n listAutomations,\n tickAutomations,\n enqueueCommand,\n listCommands,\n updateCommandStatus,\n registerSocialChannel,\n listSocialChannels\n} from \"./studioOpsStore.js\";\nimport { metrics as infraMetrics } from \"./infra/metrics.js\";\nimport { renderRhizohPrometheusMetrics, rhizohEnterpriseMetrics } from \"./infra/rhizohEnterpriseMetrics.js\";\nimport { scoreHealth } from \"./infra/healthScore.js\";\nimport { getTraceById, listTracesBySession } from \"./infra/traceRegistry.js\";\nimport { canonicalEpistemicSealString, hashAndSignEpistemicSeal, EPISTEMIC_SEAL_SCHEMA } from \"./epistemicSeal.js\";\nimport { persistEpistemicLedgerBatch } from \"./epistemicLedgerStore.js\";\nimport { persistEpistemicForecastBatch } from \"./epistemicForecastStore.js\";\nimport { buildRhizohExternalGroundTruthPayload } from \"./rhizohExternalGroundTruthGateway.js\";\nimport { ingestRhizohExternalLossBatchHttp } from \"./rhizohExternalLossIngestGateway.js\";\nimport {\n ingestRhizohProductOutcomeHttp,\n listRhizohProductOutcomeAggregates,\n verifyRhizohOutcomeHmac,\n verifyRhizohOutcomeSourceToken\n} from \"./rhizohProductOutcomeIngestGateway.js\";\nimport { initRhizoh } from \"./rhizohProductionBootstrap.js\";\nimport { handleAcademicObservatoryExportGetV0 } from \"./rhizoh/academicObservatoryHttpV0.js\";\nimport {\n handleLiveSportsBundleGetV0,\n handleLiveNewsHeadlinesGetV0,\n handleLiveWorldFeedGetV0\n} from \"./rhizoh/liveWorldFeedHttpV0.js\";\nimport {\n buildGenesisRuntimeSurfacePayload,\n recordGenesisEpistemicLedgerPersisted,\n recordGenesisEpistemicSealIssued,\n startGenesisCanonicalClock\n} from \"./genesisRuntimeSurfaceV0.js\";\nimport { registerGenesisSseClient, getGenesisContinuitySeq, setGenesisContinuityAfterPublishHook } from \"./genesisContinuityStreamHubV0.js\";\nimport { computeGenesisReplayFingerprintV0 } from \"./genesisReplayFingerprintV0.js\";\nimport { buildGenesisGatewayCapabilitiesV0 } from \"./genesisGatewayCapabilitiesV0.js\";\nimport {\n installGenesisCheckpointSurfaceGetter,\n noteGenesisCheckpointSeqCommitted,\n getLatestGenesisSignedCheckpoint\n} from \"./genesisContinuityCheckpointV0.js\";\nimport { hydrateGenesisContinuityPersistenceBootV0 } from \"./genesisContinuityHydrateBootV0.js\";\nimport { startGenesisContinuityInfraSampler } from \"./genesisContinuityInfraSamplerV0.js\";\nimport {\n genesisCheckpointQueryBySeqV0,\n genesisCheckpointQueryRangeV0,\n genesisCheckpointQueryLineageV0\n} from \"./genesisCheckpointQueryV0.js\";\nimport {\n genesisContinuityEventArchiveEnabled,\n queryGenesisContinuityEventArchiveV0,\n GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION\n} from \"./genesisContinuityEventArchiveV0.js\";\nimport { resolveGenesisReplayRouterV1, GENESIS_REPLAY_ROUTER_SCHEMA } from \"./genesisReplayRouterV1.js\";\nimport {\n computeGenesisReplayTemporalDiffV1,\n GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA\n} from \"./genesisReplayTemporalDiffV1.js\";\nimport { compareGenesisReplayEquivalenceV1 } from \"./genesisReplayEquivalenceV1.js\";\nimport { computeGenesisReplayAnalyticsV1 } from \"./genesisReplayAnalyticsV1.js\";\nimport { computeGenesisReplayEvolutionV1 } from \"./genesisReplayEvolutionV1.js\";\nimport { handleStripeCheckoutCreate, handleStripeMembershipWebhook } from \"./stripeWebhookMembership.js\";\nimport { buildRealityHealthPayload } from \"./edgeRealityConsistencyV1.js\";\nimport {\n assertWalPeerSocketAuthorityV0,\n buildGatewaySubstrateAuthoritySnapshotV0,\n logGatewaySubstrateAuthorityGuardsV0,\n validateWalPeerFeedSignatureV0\n} from \"./gatewaySubstrateAuthorityV0.js\";\nimport {\n buildSubstrateOperationalSnapshotV0,\n recordWalPeerRejectV0,\n renderSubstratePrometheusMetricsV0\n} from \"./infra/substrateOperationalMetrics.js\";\nimport { ingestClientSubstrateHealthV0 } from \"./rhizohSubstrateHealthIngestV0.js\";\nimport { recordSubstrateOtelEventV0 } from \"./infra/opentelemetrySubstrateV0.js\";\nimport {\n GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n getGenesisWorldObservationIngressStatusV0,\n ingestGenesisWorldObservationV0\n} from \"./genesisContinuityClientIngressV0.js\";\nimport {\n getGenesisContinuitySeqAuditV0,\n noteGenesisContinuitySeqAcceptedV0\n} from \"./genesisContinuitySeqGapV0.js\";\n\n/** Render / Fly / Railway: `PORT` — yoksa `CASTLE_GATEWAY_PORT` — yoksa 8090. */\nconst PORT =\n Number(process.env.PORT) ||\n Number(process.env.CASTLE_GATEWAY_PORT) ||\n 8090;\n/** Spiral / world WebSocket server — set after `wss` init; used by genesis runtime surface. */\nlet spiralWssForGenesis = null;\nconst MAX_MESSAGE_BYTES = Number(process.env.CASTLE_MAX_MESSAGE_BYTES || 32 * 1024);\nconst REQUIRED_GATEWAY_TOKEN = process.env.CASTLE_GATEWAY_TOKEN || \"\";\nconst ALLOWED_ORIGINS = parseAllowedOriginsFromEnv().fromList;\nconst httpCors = createHttpCorsPolicy();\n\n/** @returns {string|null} `Access-Control-Allow-Origin`; null = browser CORS fail. */\nfunction accessControlAllowOriginValue(req) {\n return httpCors.accessControlAllowOriginValue(req);\n}\n\nfunction applyHttpCorsHeaders(req, res) {\n return httpCors.applyHttpCorsHeaders(req, res);\n}\nconst REQUIRE_AUTH = process.env.CASTLE_REQUIRE_AUTH === \"true\";\nconst ALLOW_DEV_ANON = process.env.CASTLE_ALLOW_DEV_ANON !== \"false\";\nconst ALLOW_DEV_HTTP_UID = process.env.CASTLE_ALLOW_DEV_HTTP_UID !== \"false\";\n/** Presence mesh: allow `X-Castle-Guest-Id` when Firebase auth absent (default on). Set `false` to require auth only. */\nconst ALLOW_MESH_GUEST = process.env.CASTLE_ALLOW_MESH_GUEST !== \"false\";\nconst SOCIAL_RETRY_MAX = Math.max(0, Math.min(5, Number(process.env.CASTLE_SOCIAL_RETRY_MAX || 2)));\nconst SOCIAL_RETRY_BASE_MS = Math.max(100, Number(process.env.CASTLE_SOCIAL_RETRY_BASE_MS || 700));\nconst TELEGRAM_BOT_TOKEN = process.env.CASTLE_TELEGRAM_BOT_TOKEN || \"\";\nconst WHATSAPP_TOKEN = process.env.CASTLE_WHATSAPP_TOKEN || \"\";\nconst WHATSAPP_PHONE_NUMBER_ID = process.env.CASTLE_WHATSAPP_PHONE_NUMBER_ID || \"\";\nconst RL_RHIZOH_LLM_PER_MIN = Math.max(5, Number(process.env.CASTLE_RL_RHIZOH_LLM_PER_MIN || 40));\nconst RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN || 30)\n);\nconst RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN || 120));\nconst RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN = Math.max(\n 10,\n Number(process.env.CASTLE_RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN || 60)\n);\nconst RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN = Math.max(8, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN || 48));\nconst RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN = Math.max(4, Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN || 24));\nconst RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN || 18)\n);\nconst RL_EPISTEMIC_SEAL_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_EPISTEMIC_SEAL_PER_MIN || 120));\nconst RL_EPISTEMIC_LOG_PER_MIN = Math.max(20, Number(process.env.CASTLE_RL_EPISTEMIC_LOG_PER_MIN || 240));\nconst EPISTEMIC_SEAL_SECRET = String(\n process.env.CASTLE_EPISTEMIC_SEAL_SECRET || process.env.CASTLE_GATEWAY_TOKEN || \"\"\n).trim();\nconst RL_LLM_CONN_TEST_PER_MIN = Math.max(2, Number(process.env.CASTLE_RL_LLM_CONNECTION_TEST_PER_MIN || 8));\n\n/** One-shot production warnings: CORS surface, auth toggle, dev bypass flags (observatory deploy hygiene). */\nfunction logProductionObservatorySurfaceGuardsV0() {\n if (String(process.env.NODE_ENV || \"\").toLowerCase() !== \"production\") return;\n const tag = \"[GATEWAY_PROD_GUARD]\";\n const primaryCors = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n const corsListEmpty = ALLOWED_ORIGINS.length === 0;\n if (corsListEmpty && !primaryCors) {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty and CASTLE_HTTP_CORS_ORIGIN empty — production still merges RHIZOH_BROWSER_ORIGINS_V1 defaults; set env for preview/staging hosts`\n );\n } else if (corsListEmpty && primaryCors && primaryCors !== \"*\") {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty — only CASTLE_HTTP_CORS_ORIGIN set; add web.app + firebaseapp.com (and previews) to CASTLE_ALLOWED_ORIGINS if clients use alternate hostnames`\n );\n }\n if (primaryCors && /localhost|127\\.0\\.0\\.1/i.test(primaryCors)) {\n console.warn(`${tag} CORS: CASTLE_HTTP_CORS_ORIGIN is localhost while NODE_ENV=production`);\n }\n if (REQUIRE_AUTH) {\n console.warn(\n `${tag} AUTH: CASTLE_REQUIRE_AUTH=true — clients must send a valid gateway credential (otherwise silent 401 on replay/SSE); EventSource cannot set custom headers unless proxied`\n );\n if (REQUIRED_GATEWAY_TOKEN.length < 16) {\n console.warn(`${tag} AUTH: CASTLE_GATEWAY_TOKEN missing or short while require-auth enabled`);\n }\n }\n if (ALLOW_DEV_ANON) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_ANON is not \"false\" — dev anonymous path may be live in production (set CASTLE_ALLOW_DEV_ANON=false)`\n );\n }\n if (ALLOW_DEV_HTTP_UID) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_HTTP_UID is not \"false\" — X-Castle-Dev-Uid bypass risk (set CASTLE_ALLOW_DEV_HTTP_UID=false)`\n );\n }\n}\n\nconst rhizohRuntime = initRhizoh();\n\n/** İstemci: llmKeySource veya keySource — env | user_connection | auto */\nfunction normalizeClientLlmKeySource(raw) {\n const s = String(raw ?? \"\").trim().toLowerCase();\n if (s === \"env\" || s === \"server\") return \"env\";\n if (s === \"user_connection\" || s === \"connection\" || s === \"user\") return \"user_connection\";\n if (s === \"auto\" || s === \"\") return \"auto\";\n return null;\n}\n\n/**\n * Server-side provider wins for env/auto unless user_connection — client bundle may still send provider=openai.\n * @param {\"env\"|\"user_connection\"|\"auto\"} keyMode\n * @param {{ provider?: string, model?: string, apiKey?: string } | null} conn\n * @param {{ provider?: string, model?: string }} payload\n */\nfunction resolveServerLlmProviderV0(keyMode, conn, payload) {\n const envProvider = String(process.env.CASTLE_LLM_PROVIDER || \"\").trim() || undefined;\n const envModel = String(process.env.CASTLE_LLM_MODEL || \"\").trim() || undefined;\n\n if (keyMode === \"user_connection\") {\n return {\n resolvedProvider: conn?.provider || payload?.provider,\n resolvedModel: conn?.model || payload?.model,\n connApiKey: conn?.apiKey || \"\"\n };\n }\n\n if (keyMode === \"env\") {\n return {\n resolvedProvider: envProvider ?? payload?.provider,\n resolvedModel: envModel ?? payload?.model,\n connApiKey: \"\"\n };\n }\n\n if (conn?.provider && conn?.apiKey) {\n return {\n resolvedProvider: conn.provider,\n resolvedModel: conn.model,\n connApiKey: conn.apiKey\n };\n }\n\n return {\n resolvedProvider: envProvider ?? payload?.provider,\n resolvedModel: envModel ?? payload?.model,\n connApiKey: \"\"\n };\n}\n\nfunction jsonReplacerSafeV0(_key, value) {\n if (typeof value === \"bigint\") return Number(value);\n return value;\n}\n\nfunction sendJson(res, statusCode, payload) {\n res.writeHead(statusCode, { \"Content-Type\": \"application/json; charset=utf-8\" });\n try {\n res.end(JSON.stringify(payload, jsonReplacerSafeV0));\n } catch (error) {\n res.end(\n JSON.stringify({\n ok: false,\n error: \"response_serialize_failed\",\n detail: String(error?.message || error)\n })\n );\n }\n}\n\nfunction logRhizohHealth(stage, detail = {}) {\n try {\n console.info(`[CASTLE_${String(stage || \"unknown\")}]`, detail && typeof detail === \"object\" ? detail : {});\n } catch {\n /* noop */\n }\n}\n\nfunction renderInfraMetricsProm() {\n const lines = [\n `castle_gateway_events_processed_total ${infraMetrics.eventsProcessed}`,\n `castle_gateway_duplicate_reject_total ${infraMetrics.duplicateRejects}`,\n `castle_gateway_enqueue_latency_ms ${infraMetrics.enqueueLatencyMs}`,\n `castle_gateway_queue_depth ${infraMetrics.queueDepth}`,\n `castle_gateway_queue_lag_seconds ${(Number(infraMetrics.queueLag || 0) / 1000).toFixed(3)}`,\n `castle_gateway_errors_total ${infraMetrics.errors}`\n ];\n const buckets = infraMetrics.enqueueLatencyBuckets || {};\n for (const k of Object.keys(buckets)) {\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"${k}\"} ${buckets[k]}`);\n }\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"+Inf\"} ${infraMetrics.eventsProcessed}`);\n if (process.env.CASTLE_SUBSTRATE_METRICS !== \"0\") {\n lines.push(renderSubstratePrometheusMetricsV0().trimEnd());\n }\n return lines.join(\"\\n\");\n}\n\nasync function fetchJsonWithTimeout(url, timeoutMs = 1200) {\n const ctl = new AbortController();\n const tid = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const res = await fetch(url, { signal: ctl.signal });\n if (!res.ok) return null;\n return await res.json();\n } catch {\n return null;\n } finally {\n clearTimeout(tid);\n }\n}\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction getHttpPathname(req) {\n let p = String(req.url || \"\").split(\"?\")[0];\n /* Reverse proxies / clients sometimes append `/` — avoid silent 404 on e.g. `/rhizoh/genesis/stream/`. */\n while (p.length > 1 && p.endsWith(\"/\")) {\n p = p.slice(0, -1);\n }\n return p;\n}\n\nfunction rhizohLlmEnvConfigured() {\n const p = String(process.env.CASTLE_LLM_PROVIDER || \"openai\").toLowerCase();\n const key =\n p === \"anthropic\"\n ? process.env.ANTHROPIC_API_KEY\n : p === \"gemini\"\n ? process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY\n : p === \"xai\"\n ? process.env.XAI_API_KEY\n : p === \"deepseek\"\n ? process.env.DEEPSEEK_API_KEY\n : p === \"mistral\"\n ? process.env.MISTRAL_API_KEY\n : p === \"openrouter\"\n ? process.env.OPENROUTER_API_KEY\n : process.env.OPENAI_API_KEY;\n return !!String(key || \"\").trim();\n}\n\n/**\n * @param {import(\"firebase-admin/firestore\").Firestore | null} db\n * @param {number} timeoutMs\n */\nasync function probeFirestoreReachable(db, timeoutMs = 2000) {\n if (!db) return { ok: false, reason: \"no_db\" };\n const timeout = sleep(timeoutMs).then(() => ({ ok: false, reason: \"timeout\" }));\n const work = db\n .collection(\"_castle_gateway_health\")\n .doc(\"ping\")\n .get()\n .then(() => ({ ok: true, reason: \"reachable\" }))\n .catch((e) => ({ ok: false, reason: String(e?.message || e || \"firestore_error\") }));\n return Promise.race([work, timeout]);\n}\n\nfunction stripHtml(text = \"\") {\n return String(text)\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \" \")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \" \")\n .replace(/<[^>]+>/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction haversineMeters(aLat, aLon, bLat, bLon) {\n const R = 6371000;\n const dLat = ((bLat - aLat) * Math.PI) / 180;\n const dLon = ((bLon - aLon) * Math.PI) / 180;\n const sa = Math.sin(dLat / 2);\n const sb = Math.sin(dLon / 2);\n const x = sa * sa + Math.cos((aLat * Math.PI) / 180) * Math.cos((bLat * Math.PI) / 180) * sb * sb;\n return 2 * R * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));\n}\n\nasync function checkPublishTargetHealth(protocol, target) {\n const p = String(protocol || \"\").toUpperCase();\n const t = String(target || \"\").trim();\n if (!t) return { ok: false, reason: \"target_missing\", shouldReconnect: false };\n if (p === \"RTMP\") {\n const ok = t.startsWith(\"rtmp://\") || t.startsWith(\"rtmps://\");\n return { ok, reason: ok ? \"rtmp_target_format_ok\" : \"invalid_rtmp_target\", shouldReconnect: !ok, reconnectInSec: 15 };\n }\n if (p === \"WHIP\" || p === \"SFU\") {\n try {\n const res = await fetch(t, { method: \"OPTIONS\" });\n const ok = res.status < 500;\n return { ok, reason: `http_${res.status}`, shouldReconnect: !ok, reconnectInSec: 8 };\n } catch {\n return { ok: false, reason: \"network_unreachable\", shouldReconnect: true, reconnectInSec: 8 };\n }\n }\n return { ok: true, reason: \"unchecked_protocol\", shouldReconnect: false };\n}\n\nasync function fetchTextFromUrl(url) {\n const res = await fetch(url, { method: \"GET\" });\n if (!res.ok) throw new Error(`fetch_${res.status}`);\n const ct = String(res.headers.get(\"content-type\") || \"\");\n if (ct.includes(\"application/pdf\")) throw new Error(\"pdf_binary_not_supported_directly\");\n const body = await res.text();\n return stripHtml(body).slice(0, 14000);\n}\n\nfunction buildTaskActionGraph(task = \"\", context = {}) {\n const t = String(task || \"\").toLowerCase();\n if (t.includes(\"dinner\") || t.includes(\"akşam yemeği\") || t.includes(\"aksam yemegi\")) {\n return {\n taskTitle: \"Dinner Prep Planner\",\n nodes: [\n { id: \"n1\", title: \"Kitchen preheat\", action: \"smart-oven.preheat\", params: { temperatureC: 190, mode: \"fan\" } },\n { id: \"n2\", title: \"Lighting comfort\", action: \"smart-light.set_scene\", params: { scene: \"warm_dinner\" } },\n { id: \"n3\", title: \"Fridge inventory check\", action: \"smart-fridge.inventory_scan\", params: {} },\n { id: \"n4\", title: \"Reminder notification\", action: \"smart-watch.notify\", params: { text: \"Dinner prep started.\" } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" },\n { from: \"n3\", to: \"n4\" }\n ]\n };\n }\n return {\n taskTitle: task || \"General Planner\",\n nodes: [\n { id: \"n1\", title: \"Safety check\", action: \"device.safety_check\", params: { context } },\n { id: \"n2\", title: \"Primary action\", action: \"device.execute_task\", params: { task } },\n { id: \"n3\", title: \"Report\", action: \"studio.timeline_note\", params: { task } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" }\n ]\n };\n}\n\nfunction buildEthicsPrograms() {\n return {\n policyVersion: \"v1\",\n tracks: [\n {\n id: \"elderly-care-assist\",\n audience: \"elderly\",\n title: \"Elderly Assistance Track\",\n principles: [\"human-in-the-loop\", \"consent-first\", \"low-friction alerts\"],\n modules: [\"medication-reminder\", \"fall-risk-check\", \"family-notify\", \"daily-companion-brief\"]\n },\n {\n id: \"kids-safe-learning\",\n audience: \"children\",\n title: \"Kids Safe Learning Track\",\n principles: [\"guardian-consent\", \"age-appropriate-language\", \"content-filtering\"],\n modules: [\"study-coach\", \"screen-time-balance\", \"safe-social-share\", \"emergency-contact\"]\n }\n ]\n };\n}\n\nasync function deliverViaTelegram(channel, text) {\n const chatId = String(channel.endpoint || \"\").trim();\n if (!TELEGRAM_BOT_TOKEN) throw new Error(\"telegram_token_missing\");\n if (!chatId) throw new Error(\"telegram_chat_id_missing\");\n const url = `https://api.telegram.org/bot${encodeURIComponent(TELEGRAM_BOT_TOKEN)}/sendMessage`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ chat_id: chatId, text })\n });\n if (!res.ok) throw new Error(`telegram_http_${res.status}`);\n return { provider: \"telegram\", status: res.status };\n}\n\nasync function deliverViaWhatsapp(channel, text) {\n const to = String(channel.endpoint || \"\").trim();\n if (!WHATSAPP_TOKEN) throw new Error(\"whatsapp_token_missing\");\n if (!WHATSAPP_PHONE_NUMBER_ID) throw new Error(\"whatsapp_phone_number_id_missing\");\n if (!to) throw new Error(\"whatsapp_to_missing\");\n const url = `https://graph.facebook.com/v22.0/${encodeURIComponent(WHATSAPP_PHONE_NUMBER_ID)}/messages`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${WHATSAPP_TOKEN}` },\n body: JSON.stringify({ messaging_product: \"whatsapp\", to, type: \"text\", text: { body: text } })\n });\n if (!res.ok) throw new Error(`whatsapp_http_${res.status}`);\n return { provider: \"whatsapp\", status: res.status };\n}\n\nasync function deliverViaWebhook(channel, text) {\n const endpoint = String(channel.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"webhook_endpoint_missing\");\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text, platform: channel.platform || \"webhook\", ts: Date.now() })\n });\n if (!res.ok) throw new Error(`webhook_http_${res.status}`);\n return { provider: \"webhook\", status: res.status };\n}\n\nasync function deliverSocialMessage(channel, text) {\n const platform = String(channel.platform || \"webhook\").toLowerCase();\n if (platform === \"telegram\") return deliverViaTelegram(channel, text);\n if (platform === \"whatsapp\") return deliverViaWhatsapp(channel, text);\n return deliverViaWebhook(channel, text);\n}\n\nasync function deliverSocialWithRetry(channel, text) {\n let attempt = 0;\n let lastError = null;\n while (attempt <= SOCIAL_RETRY_MAX) {\n try {\n const result = await deliverSocialMessage(channel, text);\n return { ok: true, attempt: attempt + 1, ...result };\n } catch (error) {\n lastError = error;\n if (attempt >= SOCIAL_RETRY_MAX) break;\n const jitter = Math.floor(Math.random() * 90);\n await sleep(SOCIAL_RETRY_BASE_MS * Math.pow(2, attempt) + jitter);\n }\n attempt += 1;\n }\n return { ok: false, attempt: attempt + 1, error: String(lastError?.message || \"delivery_failed\") };\n}\n\nasync function dispatchCommandToAdapter(device, command) {\n const adapter = String(command?.adapter || device?.adapter || \"websocket\").toLowerCase();\n const endpoint = String(device?.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"device_endpoint_missing\");\n if (adapter === \"mqtt\") {\n // MQTT delivery is proxied via a bridge endpoint (HTTP webhook style).\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n type: \"mqtt-command\",\n topic: `castle/devices/${device.id}/commands`,\n action: command.action,\n params: command.params || {}\n })\n });\n if (!res.ok) throw new Error(`mqtt_bridge_http_${res.status}`);\n return { ok: true, adapter, via: \"http-bridge\", status: res.status };\n }\n if (!endpoint.startsWith(\"ws://\") && !endpoint.startsWith(\"wss://\")) throw new Error(\"invalid_websocket_endpoint\");\n const sendResult = await new Promise((resolve, reject) => {\n const ws = new WebSocket(endpoint);\n const timer = setTimeout(() => {\n try {\n ws.close();\n } catch {\n /* noop */\n }\n reject(new Error(\"websocket_timeout\"));\n }, 3000);\n ws.on(\"open\", () => {\n ws.send(\n JSON.stringify({\n type: \"castle-command\",\n commandId: command.id,\n deviceId: command.deviceId,\n action: command.action,\n params: command.params || {}\n }),\n (err) => {\n clearTimeout(timer);\n ws.close();\n if (err) reject(new Error(\"websocket_send_failed\"));\n else resolve({ ok: true, adapter: \"websocket\", via: \"ws\" });\n }\n );\n });\n ws.on(\"error\", () => {\n clearTimeout(timer);\n reject(new Error(\"websocket_connect_failed\"));\n });\n });\n return sendResult;\n}\n\nfunction readHttpJson(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => {\n if (!buf) return resolve({});\n const payload = safeJsonParse(buf);\n if (!payload || typeof payload !== \"object\") return reject(new Error(\"invalid_json_payload\"));\n resolve(payload);\n });\n req.on(\"error\", reject);\n });\n}\n\n/** Ham gövde (HMAC doğrulaması için). */\nfunction readHttpBodyText(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => resolve(buf));\n req.on(\"error\", reject);\n });\n}\n\nfunction maskOpaqueId(value) {\n const t = String(value || \"\");\n if (!t) return \"(empty)\";\n if (t.length <= 8) return \"****\";\n return `${t.slice(0, 4)}…${t.slice(-2)}`;\n}\n\nfunction readGatewayHttpToken(req) {\n const x = String(req.headers?.[\"x-castle-gateway-token\"] || \"\").trim();\n if (x) return x;\n const auth = String(req.headers?.authorization || \"\");\n const m = /^Bearer\\s+(.+)$/i.exec(auth);\n return m ? String(m[1] || \"\").trim() : \"\";\n}\n\nasync function resolveHttpUser(req) {\n const authResult = await verifyClientToken(req);\n if (authResult?.ok && authResult.user?.uid) {\n return { ok: true, uid: String(authResult.user.uid), mode: authResult.kind || \"auth\" };\n }\n if (ALLOW_DEV_ANON && ALLOW_DEV_HTTP_UID) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) return { ok: true, uid: `dev-${devUid.slice(0, 64)}`, mode: \"dev-header\" };\n }\n return { ok: false, reason: authResult?.reason || \"auth_required\" };\n}\n\n/**\n * Epistemic ledger ingest — Firebase uid OR valid transport token + X-Castle-Dev-Uid.\n * Prod disables CASTLE_ALLOW_DEV_HTTP_UID; LLM may still work while telemetry 401'd without this.\n */\nasync function resolveEpistemicIngestActor(req) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok && tok === REQUIRED_GATEWAY_TOKEN) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) {\n return { ok: true, uid: `gw-telemetry-${devUid.slice(0, 64)}`, mode: \"gateway-token-dev-uid\" };\n }\n return { ok: false, reason: \"dev_uid_required\" };\n }\n if (tok) {\n return { ok: false, reason: \"gateway_token_mismatch\" };\n }\n }\n const auth = await resolveHttpUser(req);\n if (auth.ok) return auth;\n return { ok: false, reason: auth.reason || \"auth_required\" };\n}\n\n/** Mesh actor: Firebase uid, dev header, or `guest:<uuid>` via header / `guestId` query (SSE cannot set headers). */\nasync function resolveMeshActor(req) {\n const auth = await resolveHttpUser(req);\n if (auth.ok) return { ok: true, clientUid: auth.uid, mode: auth.mode };\n if (ALLOW_MESH_GUEST) {\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const gid = String(req.headers?.[\"x-castle-guest-id\"] || q.searchParams.get(\"guestId\") || \"\").trim();\n if (gid) {\n const normalized = gid.startsWith(\"guest:\") ? gid.slice(0, 180) : `guest:${gid.slice(0, 160)}`;\n return { ok: true, clientUid: normalized, mode: \"guest-header\" };\n }\n }\n return { ok: false, reason: \"mesh_auth_required\" };\n}\n\n/** Live genesis runtime JSON (GET /rhizoh/genesis/runtime + SSE infra sampler). */\nasync function buildGenesisRuntimeSurfacePayloadLive() {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n let spiralWsActive = 0;\n if (spiralWssForGenesis) {\n for (const c of spiralWssForGenesis.clients) {\n if (c.readyState === 1) spiralWsActive += 1;\n }\n }\n const base = buildGenesisRuntimeSurfacePayload({\n infraMetrics,\n scoredHealth: scored,\n rhizohEnterpriseMetrics,\n mesh: meshContinuityAggregate(),\n workerHealth: worker,\n port: PORT,\n spiralWebSocketClientsActive: spiralWsActive\n });\n return {\n ...base,\n replayFingerprint: computeGenesisReplayFingerprintV0(base),\n gatewayCapabilities: buildGenesisGatewayCapabilitiesV0(),\n genesisStream: {\n schema: \"castle.genesis.stream_cursor.v0\",\n lastAcceptedSeq: getGenesisContinuitySeq(),\n clientIngress: {\n schema: GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n contract: \"castle.world_observation.ingress_envelope.v1\",\n postPath: rhizohRuntime.routes.genesisIngress,\n closure: getGenesisWorldObservationIngressStatusV0()\n },\n seqContinuity: getGenesisContinuitySeqAuditV0(),\n eventArchive: genesisContinuityEventArchiveEnabled()\n ? {\n enabled: true,\n projection: GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION,\n queryPath: rhizohRuntime.routes.genesisContinuityEvents\n }\n : { enabled: false },\n replayRouter: {\n schema: GENESIS_REPLAY_ROUTER_SCHEMA,\n queryPath: rhizohRuntime.routes.genesisReplay,\n queryHint: \"from=&to= or range=from-to; optional type=&checkpoints=0\",\n temporalDiffPath: rhizohRuntime.routes.genesisReplayDiff,\n temporalDiffSchema: GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA,\n equivalencePath: rhizohRuntime.routes.genesisReplayEquivalence,\n analyticsPath: rhizohRuntime.routes.genesisReplayAnalytics,\n evolutionPath: rhizohRuntime.routes.genesisReplayEvolution\n }\n }\n };\n}\n\nconst httpServer = createServer(async (req, res) => {\n const corsAllow = applyHttpCorsHeaders(req, res);\n if (req.method === \"OPTIONS\") {\n if (!corsAllow && req.headers?.origin) {\n console.warn(\"[GATEWAY_CORS] preflight rejected for origin:\", req.headers.origin);\n }\n res.writeHead(corsAllow ? 204 : 403);\n res.end();\n return;\n }\n\n const pathname = getHttpPathname(req);\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/genesis/__ping\") {\n sendJson(res, 200, {\n ok: true,\n pid: process.pid,\n entry: __filename,\n time: Date.now(),\n genesis: true\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/epistemic/__probe\") {\n sendJson(res, 200, {\n ok: true,\n epistemicIngestActorV2: true,\n routes: {\n seal: rhizohRuntime.routes.epistemicSeal,\n logsBatch: rhizohRuntime.routes.epistemicLogsBatch\n },\n hasGatewayToken: Boolean(REQUIRED_GATEWAY_TOKEN),\n gatewayTokenLen: REQUIRED_GATEWAY_TOKEN ? REQUIRED_GATEWAY_TOKEN.length : 0\n });\n return;\n }\n\n if (\n req.method === \"GET\" &&\n (pathname === \"/rhizoh/live/world-feed\" ||\n pathname === \"/rhizoh/live/sports-bundle\" ||\n pathname === \"/rhizoh/live/news-headlines\")\n ) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_live_world_feed:${ip}`, RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const out =\n pathname === \"/rhizoh/live/sports-bundle\"\n ? await handleLiveSportsBundleGetV0()\n : pathname === \"/rhizoh/live/news-headlines\"\n ? await handleLiveNewsHeadlinesGetV0(req)\n : await handleLiveWorldFeedGetV0(req);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/webhooks/stripe\") {\n await handleStripeMembershipWebhook(req, res);\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/billing/stripe/checkout\") {\n await handleStripeCheckoutCreate(req, res, { readHttpJson, resolveHttpUser, sendJson });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/live\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, live: false, dns: true, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n sendJson(res, 200, { ok: true, live: true, dns: true, service: \"castle-gateway\", ts: Date.now() });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/cors\") {\n const reqOrigin = normalizeHttpOrigin(req.headers?.origin);\n const allowOrigin = accessControlAllowOriginValue(req);\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n requestOrigin: reqOrigin || null,\n allowed: Boolean(allowOrigin),\n allowOrigin: allowOrigin || null,\n allowSetSize: httpCors.allowSet.size\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/ready\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, ready: false, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n try {\n const persistence = getFirebasePersistence();\n const llmProbe = rhizohLlmEnvConfigured();\n sendJson(res, 200, {\n ok: true,\n ready: true,\n dns: true,\n persistence: persistence.mode,\n service: \"castle-gateway\",\n gateway: \"ok\",\n llm: llmProbe ? \"ok\" : \"degraded\",\n spine: llmProbe ? \"ok\" : \"partial\"\n });\n } catch (e) {\n sendJson(res, 503, { ok: false, ready: false, reason: String(e?.message || e), service: \"castle-gateway\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/reality\") {\n const payload = buildRealityHealthPayload();\n sendJson(res, payload.ok ? 200 : 503, payload);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/substrate\") {\n const authority = buildGatewaySubstrateAuthoritySnapshotV0();\n const operational = buildSubstrateOperationalSnapshotV0();\n const drift = buildRealityHealthPayload();\n const ok = Boolean(authority.productionAuthoritySatisfied) && drift.ok;\n sendJson(res, ok ? 200 : 503, {\n ok,\n service: \"castle-gateway\",\n ts: Date.now(),\n authority,\n operational,\n drift: { ok: drift.ok, errors: drift.drift?.errors, warnings: drift.drift?.warnings }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/substrate/health\") {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const body = await readHttpJson(req);\n const uid = (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestClientSubstrateHealthV0(body, { uid, clientId: uid });\n if (!result.ok) {\n sendJson(res, 400, result);\n return;\n }\n recordSubstrateOtelEventV0(\"client_health_ingest\", { uid: String(uid).slice(0, 32) });\n sendJson(res, 200, result);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/deps\") {\n const t0 = Date.now();\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, {\n ok: false,\n dns: true,\n llm: false,\n firestore: false,\n phase: \"maintenance\",\n latencyMs: Date.now() - t0,\n service: \"castle-gateway\"\n });\n return;\n }\n const persistence = getFirebasePersistence();\n let firestoreOk = persistence.mode !== \"firebase\";\n let firestoreDetail = persistence.mode === \"file\" ? \"file_mode\" : \"not_applicable\";\n if (persistence.mode === \"firebase\" && persistence.db) {\n const pr = await probeFirestoreReachable(persistence.db, 2000);\n firestoreOk = pr.ok;\n firestoreDetail = pr.reason || (pr.ok ? \"reachable\" : \"fail\");\n } else if (persistence.mode === \"firebase\" && !persistence.db) {\n firestoreOk = false;\n firestoreDetail = \"firebase_not_initialized\";\n }\n const llmOk = rhizohLlmEnvConfigured();\n const overallOk = llmOk && firestoreOk;\n const latencyMs = Date.now() - t0;\n sendJson(res, 200, {\n ok: overallOk,\n dns: true,\n llm: llmOk,\n firestore: persistence.mode === \"firebase\" ? firestoreOk : false,\n persistence: persistence.mode,\n firestoreDetail,\n latencyMs,\n service: \"castle-gateway\",\n wsPort: PORT\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health\") {\n const persistence = getFirebasePersistence();\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n wsPort: PORT,\n persistence: persistence.mode,\n presenceMesh: \"sse\"\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/health\") {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n sendJson(res, 200, {\n ok: true,\n role: \"gateway\",\n queueLagMs: infraMetrics.queueLag,\n queueDepth: infraMetrics.queueDepth,\n errors: infraMetrics.errors,\n status: scored.status,\n score: scored.score,\n reasons: scored.reasons,\n worker: worker\n ? {\n status: worker.status,\n score: worker.score,\n reasons: worker.reasons\n }\n : null,\n timestamp: Date.now()\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisRuntime) {\n const payload = await buildGenesisRuntimeSurfacePayloadLive();\n sendJson(res, 200, payload);\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.genesisIngress) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const actor = await resolveMeshActor(req);\n const clientId = actor.ok ? actor.clientUid : (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestGenesisWorldObservationV0(body, { clientId });\n if (!result.ok) {\n const status =\n result.error === \"rate_limited\" || result.error === \"agent_spoke_throttled\"\n ? 429\n : result.error === \"payload_too_large\"\n ? 413\n : 400;\n sendJson(res, status, result);\n return;\n }\n sendJson(res, result.idempotent ? 200 : 202, result);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisStream) {\n const corsOrigin = accessControlAllowOriginValue(req);\n if (!corsOrigin && String(process.env.NODE_ENV || \"\").toLowerCase() === \"production\") {\n const og = String(req.headers?.origin || \"\").trim();\n if (og) {\n console.warn(\"[GATEWAY] genesis SSE: missing Access-Control-Allow-Origin for browser Origin (CORS whitelist mismatch):\", og);\n }\n }\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n res.write(\"retry: 3000\\n\\n\");\n res.write(\": genesis continuity stream\\n\\n\");\n res.write(\"event: genesis\\n\");\n res.write(`data: {\"booted\":true,\"pid\":${process.pid}}\\n\\n`);\n registerGenesisSseClient(res, req);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLatest) {\n const cp = getLatestGenesisSignedCheckpoint();\n if (!cp) {\n sendJson(res, 404, { ok: false, error: \"no_checkpoint_yet\", hint: \"wait_for_seq_interval_or_configure_signing_secret\" });\n return;\n }\n sendJson(res, 200, { ok: true, checkpoint: cp });\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(rhizohRuntime.routes.genesisCheckpointBySeqPrefix)) {\n const tail = pathname.slice(rhizohRuntime.routes.genesisCheckpointBySeqPrefix.length).split(\"/\")[0];\n const seq = parseInt(String(tail || \"\").trim(), 10);\n const out = await genesisCheckpointQueryBySeqV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"checkpoint_not_found\") {\n sendJson(res, 404, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointRange) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryRangeV0(from, to);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"range_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLineage) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const seq = parseInt(String(u.searchParams.get(\"seq\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryLineageV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"lineage_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisContinuityEvents) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const limit = parseInt(String(u.searchParams.get(\"limit\") || \"\").trim(), 10);\n const out = await queryGenesisContinuityEventArchiveV0(from, to, type, limit);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\" || out.error === \"event_archive_disabled\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"archive_file_too_large\") {\n sendJson(res, 413, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplay) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await resolveGenesisReplayRouterV1({\n from: fromSeq,\n to: toSeq,\n type,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayDiff) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const out = await computeGenesisReplayTemporalDiffV1({ from: fromSeq, to: toSeq, type });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEquivalence) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from1 = parseInt(String(u.searchParams.get(\"from1\") || \"\").trim(), 10);\n const to1 = parseInt(String(u.searchParams.get(\"to1\") || \"\").trim(), 10);\n const from2 = parseInt(String(u.searchParams.get(\"from2\") || \"\").trim(), 10);\n const to2 = parseInt(String(u.searchParams.get(\"to2\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await compareGenesisReplayEquivalenceV1({\n from1,\n to1,\n from2,\n to2,\n type,\n includeCheckpoints\n });\n if (\n !out.ok &&\n (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")\n ) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayAnalytics) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayAnalyticsV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEvolution) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const collapseWindows = parseInt(String(u.searchParams.get(\"collapseWindows\") || \"0\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayEvolutionV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n collapseWindows,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/metrics\") {\n res.writeHead(200, { \"Content-Type\": \"text/plain; version=0.0.4; charset=utf-8\" });\n res.end(renderInfraMetricsProm());\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.infraPrometheusMetrics) {\n if (process.env.CASTLE_PROMETHEUS_METRICS !== \"1\") {\n sendJson(res, 404, { ok: false, error: \"prometheus_metrics_disabled\" });\n return;\n }\n res.writeHead(200, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(renderRhizohPrometheusMetrics());\n return;\n }\n if (req.method === \"GET\" && pathname.startsWith(\"/infra/traces/\")) {\n const traceId = decodeURIComponent(pathname.slice(\"/infra/traces/\".length));\n const item = await getTraceById(traceId);\n if (!item) return sendJson(res, 404, { ok: false, error: \"trace_not_found\" });\n return sendJson(res, 200, { ok: true, trace: item });\n }\n if (req.method === \"GET\" && pathname === \"/infra/traces\") {\n const u = new URL(String(req.url || \"/infra/traces\"), `http://${req.headers.host || \"localhost\"}`);\n const sessionId = String(u.searchParams.get(\"sessionId\") || \"\").trim();\n const items = sessionId ? await listTracesBySession(sessionId) : [];\n return sendJson(res, 200, { ok: true, sessionId, traces: items });\n }\n\n /** Presence room mesh (C): JOIN / LEAVE / SNAPSHOT / DELTA / REPLAY (HTTP); SUBSCRIBE = SSE stream. */\n if (req.method === \"POST\" && pathname === \"/presence/mesh/join\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid, clientUid: actor.clientUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/leave\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshLeave(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/snapshot\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n sendJson(res, 200, meshSnapshot(roomUid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/delta\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, MAX_MESSAGE_BYTES);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const seq = meshAppendDelta(roomUid, actor.clientUid, {\n node: body?.node,\n projectionPatch: body?.projectionPatch,\n writerSubject: body?.writerSubject\n });\n sendJson(res, 200, { ok: true, roomUid, seq });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/replay\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const fromNodeId = body?.fromNodeId != null ? String(body.fromNodeId) : \"\";\n const fromSeq = body?.fromSeq;\n if (fromNodeId) {\n sendJson(res, 200, meshReplayFromNodeId(roomUid, fromNodeId));\n return;\n }\n sendJson(res, 200, meshReplayFromSeq(roomUid, fromSeq));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/presence/mesh/subscribe\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const roomUid = String(q.searchParams.get(\"roomUid\") || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n const write = (chunk) => {\n try {\n res.write(chunk);\n } catch {\n /* closed */\n }\n };\n const unsub = meshSubscribeSse(roomUid, write);\n const onClose = () => {\n unsub();\n meshLeave(roomUid, actor.clientUid);\n try {\n res.end();\n } catch {\n /* ignore */\n }\n };\n req.on(\"close\", onClose);\n req.on(\"aborted\", onClose);\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, uid: auth.uid, mode: auth.mode, items: await listConnections(auth.uid) });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const id = await createConnection(auth.uid, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"create_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n await updateConnection(auth.uid, id, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"update_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"DELETE\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n await deleteConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"delete_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url?.startsWith(\"/rhizoh/network/presence\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const u = new URL(req.url, \"http://localhost\");\n const roomKey = String(u.searchParams.get(\"room\") || \"world_space_c2c_v0\").slice(0, 64);\n sendJson(res, 200, {\n ok: true,\n roomKey,\n presence: listCastleNetworkPresenceV0(roomKey)\n });\n } catch (error) {\n sendJson(res, 500, { ok: false, error: error?.message || \"presence_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/rhizoh/sync/vault\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const snapshot = await getCloudSyncSnapshotV0(auth.uid);\n sendJson(res, 200, { ok: true, uid: auth.uid, snapshot });\n } catch (error) {\n sendJson(res, 500, { ok: false, error: error?.message || \"cloud_sync_pull_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/rhizoh/sync/vault\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const snapshot = await mergeCloudSyncSnapshotV0(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, uid: auth.uid, snapshot });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"cloud_sync_push_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections/test\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n if (!checkHttpRateLimit(`llm_conn_test:${auth.uid}`, RL_LLM_CONN_TEST_PER_MIN, 60_000)) {\n return sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n }\n const hasBodyKey = !!String(payload?.apiKey || \"\").trim();\n const result = await queryRhizohLlm(\n {\n message: String(payload?.message || \"Ping from Castle Gateway\"),\n context: payload?.context || { source: \"connection_test\" },\n provider: payload?.provider,\n model: payload?.model,\n apiKey: payload?.apiKey || \"\"\n },\n { allowExternalApiKey: true, llmKeySource: hasBodyKey ? \"auto\" : \"env\" }\n );\n logLlmAccess({\n route: \"/llm/connections/test\",\n uid: auth.uid,\n llmKeyBillingOwner: result.llmKeyBillingOwner,\n llmKeyOrigin: result.llmKeyOrigin,\n provider: result.provider,\n model: result.model,\n connectionId: null\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"connection_test_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAgentIdentities(auth.uid);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerAgentIdentity({\n ownerUid: auth.uid,\n agentId: payload?.agentId,\n role: payload?.role,\n personaSeed: payload?.personaSeed || {},\n capabilityLevel: payload?.capabilityLevel\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/agents/identities/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const owns = await getAgentIdentity(id);\n if (!owns || owns.ownerUid !== auth.uid) return sendJson(res, 403, { ok: false, error: \"forbidden\" });\n const payload = await readHttpJson(req);\n const row = await updateAgentIdentity(id, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/academy/events\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/academy/events\"}`);\n const status = String(url.searchParams.get(\"status\") || \"\");\n const items = await listAcademyEvents({ ownerUid: auth.uid, status, limit: 120 });\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/academy/events\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const ev = await queueAcademyEvent({\n type: payload?.type,\n roomId: payload?.roomId,\n ownerUid: auth.uid,\n participants: payload?.participants || [],\n topic: payload?.topic || \"academy\",\n payload: payload?.payload || {}\n });\n sendJson(res, 200, { ok: true, event: ev });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && String(req.url || \"\").startsWith(\"/academy/events/\") && String(req.url || \"\").endsWith(\"/resolve\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const parts = String(req.url || \"\").split(\"/\").filter(Boolean);\n const eventId = parts[2] || \"\";\n try {\n const payload = await readHttpJson(req);\n const out = await resolveAcademyEvent(eventId, payload || {});\n sendJson(res, 200, { ok: true, event: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_resolve_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/memory\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/memory\"}`);\n const scope = url.searchParams.get(\"scope\") === \"agents\" ? \"agents\" : \"users\";\n const id = String(url.searchParams.get(\"id\") || (scope === \"agents\" ? \"\" : auth.uid));\n const kind = String(url.searchParams.get(\"kind\") || \"\");\n const limit = Number(url.searchParams.get(\"limit\") || 40);\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n sendJson(res, 200, { ok: true, scope, id, kind, items: await listMemories({ scope, id, kind, limit }) });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const profile = await getPersonaGoalMemory(auth.uid);\n sendJson(res, 200, { ok: true, profile });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const profile = await setPersonaGoalMemory({\n uid: auth.uid,\n personaPatch: payload?.personaPatch || {},\n goals: Array.isArray(payload?.goals) ? payload.goals : null,\n preferencesPatch: payload?.preferencesPatch || {}\n });\n sendJson(res, 200, { ok: true, profile });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"profile_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/compact\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const out = await autoCompactMemories({ scope, id });\n sendJson(res, 200, { ok: true, scope, id, ...out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_compact_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/context\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const context = await getMemoryContext({\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n query: String(payload?.query || \"\"),\n limit: Number(payload?.limit || 80)\n });\n sendJson(res, 200, { ok: true, context });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_context_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/place-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const placeName = String(payload?.placeName || \"\").trim();\n if (!placeName) return sendJson(res, 400, { ok: false, error: \"placeName_required\" });\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const persona = String(payload?.waypointPersona || payload?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(placeName)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Place: ${placeName}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Give a concise conversational briefing about this place for a walking-route companion.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-place-brief\", placeName, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `PLACE_BRIEF:${placeName} => ${String(summary.reply || \"\").slice(0, 900)}`,\n tags: [\"place\", \"brief\", \"event-layer\"],\n importance: 0.62,\n meta: { placeName }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"place-brief\",\n text: `${placeName} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: {\n placeName,\n directive: summary.directive,\n lat: hit?.lat ? Number(hit.lat) : null,\n lon: hit?.lon ? Number(hit.lon) : null\n }\n });\n sendJson(res, 200, { ok: true, place: hit || null, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"place_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/route-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const waypoints = Array.isArray(payload?.waypoints) ? payload.waypoints.slice(0, 24) : [];\n const out = [];\n for (const wp of waypoints) {\n const name = String(wp?.name || \"\").trim();\n if (!name) continue;\n const persona = String(wp?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(name)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Waypoint: ${name}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Generate a concise spoken briefing for route waypoint.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-route-brief\", waypoint: name, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n out.push({\n name,\n persona,\n place: hit || null,\n reply: summary.reply,\n directive: summary.directive\n });\n }\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `ROUTE_BRIEF: waypoints=${out.length} ${out.map((x) => `${x.name}:${x.persona}`).join(\" | \")}`.slice(0, 1300),\n tags: [\"route\", \"waypoint\", \"brief\", \"event-layer\"],\n importance: 0.74,\n meta: { count: out.length }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"route-brief\",\n text: out.map((x) => `${x.name}:${String(x.reply || \"\").slice(0, 140)}`).join(\" | \").slice(0, 1400),\n roomId: \"studio-main\",\n meta: {\n waypointCount: out.length,\n waypoints: out.map((x) => ({\n name: x.name,\n persona: x.persona,\n lat: x.place?.lat ? Number(x.place.lat) : null,\n lon: x.place?.lon ? Number(x.place.lon) : null\n }))\n }\n });\n sendJson(res, 200, { ok: true, items: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"route_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/pdf-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const title = String(payload?.title || \"document\");\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const providedText = String(payload?.text || \"\");\n const url = String(payload?.url || \"\");\n let content = providedText;\n if (!content && url) content = await fetchTextFromUrl(url);\n if (!content) return sendJson(res, 400, { ok: false, error: \"text_or_supported_url_required\" });\n const summary = await queryRhizohLlm(\n {\n message: `Summarize this document and provide key points + practical actions.\\nTitle:${title}\\nContent:\\n${content.slice(0, 12000)}`,\n context: { source: \"event-layer-pdf-brief\", title },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `DOC_BRIEF:${title} => ${String(summary.reply || \"\").slice(0, 1000)}`,\n tags: [\"document\", \"brief\", \"library\", \"event-layer\"],\n importance: 0.7,\n meta: { title, url: url || null }\n });\n await addTranscript(auth.uid, {\n source: \"library-brief\",\n eventType: \"pdf-brief\",\n text: `${title} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { title }\n });\n sendJson(res, 200, { ok: true, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/pdf/upload\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req, 8 * 1024 * 1024);\n const job = await enqueuePdfJob({\n uid: auth.uid,\n fileName: payload?.fileName || \"document.pdf\",\n bytesBase64: payload?.contentBase64\n });\n sendJson(res, 200, { ok: true, job });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_upload_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/session\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createPublishSession(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session\",\n text: `Publish session armed: ${row.protocol} ${row.target}`.slice(0, 500),\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: row.id, bridge: row.bridge }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/studio/publish/sessions\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listPublishSessions(auth.uid, 80);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/health-check\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const protocol = String(payload?.protocol || \"WHIP\");\n const target = String(payload?.target || \"\");\n const health = await checkPublishTargetHealth(protocol, target);\n if (payload?.sessionId) {\n await updatePublishSession(auth.uid, String(payload.sessionId), {\n status: health.ok ? \"healthy\" : \"degraded\",\n reconnectPolicy: {\n enabled: true,\n backoffSec: [2, 5, 10, 20],\n nextRetryInSec: health.reconnectInSec || 0,\n maxRetries: 8,\n reason: health.reason\n }\n });\n }\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-health\",\n text: `${protocol} ${target} => ${health.ok ? \"healthy\" : \"degraded\"} (${health.reason})`,\n roomId: \"studio-main\",\n meta: { protocol, target, reconnect: !!health.shouldReconnect }\n });\n sendJson(res, 200, { ok: true, health });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_health_check_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/studio/publish/sessions/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updatePublishSession(auth.uid, id, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session-update\",\n text: `Session ${id} -> ${row.status || \"updated\"}`,\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: id, status: row.status, ingestUrl: row.ingestUrl || \"\" }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/transcripts\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/transcripts\"}`);\n const limit = Math.max(1, Math.min(200, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await listTranscripts(auth.uid, limit);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/transcripts\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await addTranscript(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"transcript_write_failed\" });\n }\n return;\n }\n\n /** E1: broadcast intent → GreenRoom capability → transcript + memory artifact */\n if (req.method === \"POST\" && req.url === \"/studio/capabilities/greenroom\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const traceId = String(payload?.traceId || `TRC-${Date.now().toString(36).toUpperCase()}`);\n const title = String(payload?.title || \"Castle Live Broadcast\").slice(0, 200);\n const intentRaw = String(payload?.intentRaw || \"\").slice(0, 800);\n const audienceEstimate = Math.max(0, Math.min(50_000_000, Number(payload?.audienceEstimate) || 1200));\n const roomId = String(payload?.roomId || \"greenroom-main\");\n const transcriptRow = await addTranscript(auth.uid, {\n source: \"rhizoh\",\n eventType: \"BROADCAST_ROUTED\",\n text: `GreenRoom · ${title}${intentRaw ? `\\nIntent: ${intentRaw}` : \"\"}`,\n roomId,\n meta: {\n ack: \"BROADCAST_ROUTED\",\n capability: \"greenroom\",\n intentType: \"broadcast\",\n traceId,\n audienceEstimate,\n artifact: {\n kind: \"broadcast_card\",\n title,\n traceId,\n roomId,\n createdAt: Date.now()\n }\n }\n });\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n text: `GREENROOM_BROADCAST:${title} trace=${traceId}`,\n tags: [\"broadcast\", \"greenroom\", \"artifact\", \"castle-library\"],\n importance: 0.72,\n kind: \"episodic\",\n meta: { traceId, capability: \"greenroom\", transcriptId: transcriptRow.id }\n });\n sendJson(res, 200, {\n ok: true,\n ack: \"BROADCAST_ROUTED\",\n traceId,\n transcript: transcriptRow,\n replayPath: `/replay/${encodeURIComponent(traceId)}`,\n sharePath: `/replay/${encodeURIComponent(traceId)}`\n });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"greenroom_capability_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/metadata/search\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/metadata/search\"}`);\n const q = String(url.searchParams.get(\"q\") || \"\");\n const limit = Math.max(1, Math.min(300, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await searchMetadata(auth.uid, q, limit);\n sendJson(res, 200, { ok: true, q, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerDevice(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, device: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"device_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const lat = Number(payload?.lat);\n const lon = Number(payload?.lon);\n const speed = Math.max(0, Number(payload?.speed || 0));\n const cfgCenterLat = Number(process.env.CASTLE_GEOFENCE_CENTER_LAT || 41.0082);\n const cfgCenterLon = Number(process.env.CASTLE_GEOFENCE_CENTER_LON || 28.9784);\n const cfgRadiusM = Math.max(100, Number(process.env.CASTLE_GEOFENCE_RADIUS_M || 55000));\n const cfgSpeedCap = Math.max(1, Number(process.env.CASTLE_SPEED_CAP_MPS || 35));\n const dist = Number.isFinite(lat) && Number.isFinite(lon) ? haversineMeters(cfgCenterLat, cfgCenterLon, lat, lon) : 0;\n const geofenceOk = !Number.isFinite(dist) ? true : dist <= cfgRadiusM;\n const speedOk = speed <= cfgSpeedCap;\n const failsafe = !(geofenceOk && speedOk);\n const safety = {\n geofenceOk,\n speedOk,\n failsafe,\n distanceFromCenterM: Number.isFinite(dist) ? dist : null,\n speedCapMps: cfgSpeedCap\n };\n const row = await appendTelemetry(auth.uid, deviceId, {\n lat,\n lon,\n speed,\n battery: payload?.battery,\n mode: payload?.mode || (failsafe ? \"FAILSAFE_HOLD\" : \"ACTIVE\"),\n raw: payload?.raw || {}\n });\n await updateDevice(auth.uid, deviceId, {\n status: failsafe ? \"failsafe\" : \"active\",\n lastTelemetryAt: row.ts,\n safetyState: safety\n });\n if (failsafe) {\n await addTranscript(auth.uid, {\n source: \"robotics-guardrails\",\n eventType: \"failsafe\",\n text: `Device ${deviceId} failsafe triggered geofenceOk=${geofenceOk} speedOk=${speedOk}`,\n roomId: \"studio-main\",\n meta: { deviceId, safety }\n });\n }\n sendJson(res, 200, { ok: true, telemetry: row, safety, command: failsafe ? \"FAILSAFE_HOLD\" : \"CONTINUE\" });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"telemetry_ingest_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listTelemetry(auth.uid, 150);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createAutomation(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"automation-scheduled\",\n text: `Automation scheduled: ${row.title} at ${new Date(row.scheduleAt).toLocaleString()}`,\n roomId: \"studio-main\",\n meta: { automationId: row.id, action: row.action, deviceId: row.deviceId }\n });\n sendJson(res, 200, { ok: true, automation: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"automation_create_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAutomations(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listDevices(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const devs = await listDevices(auth.uid, 500);\n const d = devs.find((x) => x.id === deviceId);\n if (!d) return sendJson(res, 404, { ok: false, error: \"device_not_found\" });\n const action = String(payload?.action || \"\");\n const caps = Array.isArray(d.capabilityProfile) ? d.capabilityProfile : [];\n if (action && caps.length && !caps.includes(action) && !caps.includes(\"*\")) {\n return sendJson(res, 400, { ok: false, error: \"unsupported_action_for_device\", capabilityProfile: caps });\n }\n const cmd = await enqueueCommand(auth.uid, {\n deviceId,\n action: action || \"NOOP\",\n params: payload?.params || {},\n adapter: payload?.adapter || d.adapter || \"websocket\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-command-queue\",\n eventType: \"command-queued\",\n text: `${cmd.deviceId} -> ${cmd.action}`,\n roomId: \"studio-main\",\n meta: { commandId: cmd.id, adapter: cmd.adapter }\n });\n sendJson(res, 200, { ok: true, command: cmd });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listCommands(auth.uid, 200);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/robotics/commands/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updateCommandStatus(auth.uid, id, payload?.status || \"sent\");\n sendJson(res, 200, { ok: true, command: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/rhizoh/bridge\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const result = await runRhizohBrainV2({\n message: String(payload?.message || \"robotics bridge command\"),\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n worldState: payload?.worldState || {},\n deviceState: payload?.deviceState || {},\n intent: \"robotics-mechanics\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-bridge\",\n eventType: \"robotics-command\",\n text: `Bridge -> ${String(result.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { directive: result.directive, agentId: payload?.agentId || \"\" }\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"robotics_bridge_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/planner/graph\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const graph = buildTaskActionGraph(String(payload?.task || \"\"), payload?.context || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"planner-graph\",\n text: `Planner graph built: ${graph.taskTitle} nodes=${graph.nodes.length}`,\n roomId: \"studio-main\",\n meta: { taskTitle: graph.taskTitle, nodeCount: graph.nodes.length }\n });\n sendJson(res, 200, { ok: true, graph });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"planner_graph_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerSocialChannel(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, channel: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_channel_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listSocialChannels(auth.uid, 100);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/broadcast\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const text = String(payload?.text || \"\").slice(0, 1200);\n const channels = await listSocialChannels(auth.uid, 100);\n const enabled = channels.filter((c) => c.status === \"enabled\");\n const deliveries = [];\n for (const c of enabled) {\n const out = await deliverSocialWithRetry(c, text);\n deliveries.push({ id: c.id, platform: c.platform, ...out });\n }\n const okCount = deliveries.filter((d) => d.ok).length;\n await addTranscript(auth.uid, {\n source: \"social-layer\",\n eventType: \"social-broadcast\",\n text: `Broadcast delivered ${okCount}/${enabled.length} channel(s): ${text.slice(0, 240)}`,\n roomId: \"studio-main\",\n meta: { deliveries }\n });\n sendJson(res, 200, { ok: true, sentChannels: okCount, totalChannels: enabled.length, deliveries });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_broadcast_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/ethics/programs\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, ...buildEthicsPrograms() });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands/dispatch\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const all = await listCommands(auth.uid, 200);\n const queued = all.filter((c) => String(c.status || \"queued\") === \"queued\").slice(0, 50);\n const devices = await listDevices(auth.uid, 500);\n const byId = new Map(devices.map((d) => [d.id, d]));\n const results = [];\n for (const cmd of queued) {\n const dev = byId.get(cmd.deviceId);\n if (!dev) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: \"device_not_found\" });\n continue;\n }\n try {\n const dispatched = await dispatchCommandToAdapter(dev, cmd);\n await updateCommandStatus(auth.uid, cmd.id, \"sent\");\n results.push({ id: cmd.id, ok: true, adapter: dispatched.adapter, via: dispatched.via });\n } catch (error) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: String(error?.message || \"dispatch_failed\") });\n }\n }\n await addTranscript(auth.uid, {\n source: \"robotics-command-dispatch\",\n eventType: \"command-dispatch-batch\",\n text: `Dispatch batch size=${results.length} ok=${results.filter((x) => x.ok).length}`,\n roomId: \"studio-main\",\n meta: { results: results.slice(0, 30) }\n });\n sendJson(res, 200, { ok: true, processed: results.length, results });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"dispatch_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/pdf/jobs/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n const job = await getPdfJob(auth.uid, id);\n if (!job) return sendJson(res, 404, { ok: false, error: \"pdf_job_not_found\" });\n sendJson(res, 200, { ok: true, job });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const row = await appendMemory({\n scope,\n id,\n text: payload?.text,\n tags: payload?.tags || [],\n importance: payload?.importance,\n kind: payload?.kind || \"episodic\",\n meta: { uid: auth.uid, ...(payload?.meta || {}) }\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.externalTruth) {\n try {\n if (process.env.RHIZOH_EXTERNAL_TRUTH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_truth:${ip}`, RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n sendJson(res, 200, buildRhizohExternalGroundTruthPayload(req));\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.productOutcome) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const rawBody = await readHttpBodyText(req, 64 * 1024);\n const body = rawBody.trim() ? safeJsonParse(rawBody) : null;\n if (!body || typeof body !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"invalid_json_payload\" });\n return;\n }\n const sigHdr = String(req.headers?.[\"x-rhizoh-outcome-signature\"] || \"\").trim();\n const hmacV = verifyRhizohOutcomeHmac(rawBody, sigHdr);\n if (!hmacV.ok) {\n sendJson(res, 401, { ok: false, error: hmacV.error || \"outcome_hmac_invalid\" });\n return;\n }\n const srcTokHdr = String(req.headers?.[\"x-rhizoh-outcome-source-token\"] || \"\").trim();\n const srcV = verifyRhizohOutcomeSourceToken(body.source, srcTokHdr);\n if (!srcV.ok) {\n sendJson(res, 401, { ok: false, error: srcV.error || \"outcome_source_token_invalid\" });\n return;\n }\n const subjectKey = String(body.subjectKey || \"\").trim().slice(0, 128);\n if (\n subjectKey &&\n !checkHttpRateLimit(\n `rhizoh_product_outcome_subj:${subjectKey}`,\n RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN,\n 60_000\n )\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_subject\" });\n return;\n }\n const result = ingestRhizohProductOutcomeHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n schemaVersion: \"1.1.0\",\n aggregate: result.aggregate,\n integrity: result.integrity\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"outcome_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.productOutcomeAggregate) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome_agg:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const u = new URL(String(req.url || \"/\"), \"http://localhost\");\n const cohortId = u.searchParams.get(\"cohortId\") || \"\";\n const decisionFingerprint = u.searchParams.get(\"decisionFingerprint\") || \"\";\n const limit = u.searchParams.get(\"limit\") || \"50\";\n const rows = listRhizohProductOutcomeAggregates({\n cohortId: cohortId || undefined,\n decisionFingerprint: decisionFingerprint || undefined,\n limit: Number(limit) || 50\n });\n sendJson(res, 200, { ok: true, schemaVersion: \"1.1.0\", rows });\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.externalLossBatch) {\n try {\n if (process.env.RHIZOH_EXTERNAL_LOSS_BATCH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_loss_batch:${ip}`, RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const result = ingestRhizohExternalLossBatchHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, { ok: true, accepted: result.accepted ?? 0 });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"batch_failed\") });\n }\n return;\n }\n\n function moderationAdminOk(req) {\n const expected = String(process.env.CASTLE_MODERATION_ADMIN_KEY || \"\").trim();\n if (!expected) return false;\n const got = String(req.headers[\"x-castle-moderation-key\"] || \"\").trim();\n return got.length > 0 && got === expected;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.opsAbuseReport) {\n try {\n const body = await readHttpJson(req, 32 * 1024);\n const auth = await resolveHttpUser(req);\n const entry = submitAbuseReportV0({\n reporterUid: auth.ok ? auth.uid : null,\n targetUid: body?.targetUid ?? null,\n category: body?.category,\n detail: body?.detail,\n traceId: body?.traceId ?? null\n });\n sendJson(res, 202, { ok: true, reportId: entry.id, status: entry.status });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsModerationQueue) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n queue: listModerationQueueV0(100),\n softBlocked: listSoftBlockedUsersV0()\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-block\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const out = softBlockUserV0(body?.uid, body?.reason);\n sendJson(res, out.ok ? 200 : 400, out);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-unblock\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n sendJson(res, 200, removeSoftBlockV0(body?.uid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsAgentSnapshots) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n const limit = Number(new URL(req.url || \"\", \"http://local\").searchParams.get(\"limit\") || 50);\n sendJson(res, 200, { ok: true, snapshots: listRecentAgentSnapshotsV0(limit) });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.academicObservatoryExport) {\n const auth = await resolveHttpUser(req);\n const out = handleAcademicObservatoryExportGetV0(req, auth);\n sendJson(res, out.status, out.body);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsHardeningStatus) {\n const auth = await resolveHttpUser(req);\n const principal = auth.ok ? `uid:${auth.uid}` : null;\n const gclSnap = await getGlobalCostLedgerSnapshotV0(principal);\n const economicStrategy = evaluateEconomicStrategyL3V0(gclSnap);\n const hardeningTenantId = resolveHardeningTenantIdV0(auth, req);\n const unifiedState = await buildUnifiedStateNarrativeV0({\n tenantId: hardeningTenantId ?? undefined,\n platformScope: !hardeningTenantId,\n principal\n });\n sendJson(res, 200, {\n ok: true,\n schema: \"rhizoh.ops.hardening_status.v0\",\n containment: readAgentContainmentConfigV0(),\n cost: readCostContainmentConfigV0(),\n gcl: {\n backend: await getGclBackendStatusV0(),\n snapshot: gclSnap\n },\n economicStrategyL3: economicStrategy,\n phasedRollout: await getPhasedRolloutStatsV0(),\n tier: readPhasedRolloutTierV0(),\n principalCost: auth.ok ? await getCostContainmentStatsV0(principal) : null,\n stressTaxonomy: verifyStressTaxonomyCoverageV0(),\n stressConflictResolution: verifyStressConflictResolutionV0(),\n resolutionStabilityEnvelope: verifyResolutionStabilityEnvelopeV0(),\n unifiedState: {\n tenantScope: {\n tenantId: unifiedState.tenantScope?.tenantId,\n scope: unifiedState.tenantScope?.scope,\n noGlobalDerivedState: unifiedState.tenantScope?.noGlobalDerivedState\n },\n narrativeFingerprint: unifiedState.narrativeFingerprint,\n screenshotScopeWatermark: unifiedState.screenshotScopeWatermark,\n interpretationSafetyContract: unifiedState.interpretationSafetyContract,\n interpretationUxContract: {\n schema: unifiedState.interpretationUxContract?.schema,\n layerOrder: unifiedState.interpretationUxContract?.layerOrder,\n narrativeNeverVisuallyDominant: unifiedState.interpretationUxContract?.narrativeNeverVisuallyDominant\n },\n governance: {\n narrativeIsNotDecisionLayer: unifiedState.governance.narrativeIsNotDecisionLayer,\n displayOrder: unifiedState.governance.displayOrder,\n trustPosture: unifiedState.governance.trustPosture,\n interpretationUxContract: unifiedState.governance.interpretationUxContract?.schema\n },\n humanOps: {\n decisionTier: unifiedState.humanOps?.humanDecisionScaling?.tier,\n decisionOwner: unifiedState.humanOps?.humanDecisionScaling?.decisionOwner,\n opsRouteId: unifiedState.humanOps?.humanDecisionOpsRunbook?.routing?.routeId,\n slaAckMinutes: unifiedState.humanOps?.humanDecisionOpsRunbook?.sla?.ackMinutes,\n uiDisclaimerRequired: unifiedState.humanOps?.narrativeUiSafety?.displayRules?.requireDisclaimerBanner,\n misreadHighResidual: unifiedState.humanOps?.misreadSimulation?.highResidualCount,\n propagationHighResidual: unifiedState.humanOps?.socialPropagationSimulation?.highResidualCount,\n worstWatermarkSurvivability:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.worstWatermarkSurvivability,\n dominantDistortionSource:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.dominantDistortionSource\n },\n culturalRisk: {\n perceptualRiskClass: unifiedState.culturalRisk?.perceptualRiskClass,\n trustFork: unifiedState.culturalRisk?.trustDynamics?.fork,\n trustDecayScore: unifiedState.culturalRisk?.trustDynamics?.scores?.trustDecay,\n mythologyScore: unifiedState.culturalRisk?.trustDynamics?.scores?.mythology,\n confidenceDetachmentExposure:\n unifiedState.culturalRisk?.trustDynamics?.axes?.confidenceDetachmentExposure\n },\n appliedSystemsLayer: {\n schema: unifiedState.appliedSystemsLayer?.schema,\n researchOutputType: unifiedState.appliedSystemsLayer?.researchGovernance?.outputType,\n roboticsBlockActuation: unifiedState.appliedSystemsLayer?.roboticsGrounding?.blockActuation,\n spiralRumorCount:\n unifiedState.appliedSystemsLayer?.spiralMMOGameKernel?.gameLayer?.rumors?.length\n },\n actionSemanticGovernance: {\n schema: unifiedState.actionSemanticGovernance?.schema,\n highRiskActionCount: unifiedState.actionSemanticGovernance?.highRiskActionCount,\n loopClosed: unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.loopClosed,\n authorityCollapseMitigated:\n unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.risks?.authorityCollapse\n ?.mitigated\n },\n actionContextResolution: {\n schema: unifiedState.actionContextResolution?.schema,\n contextFingerprint: unifiedState.actionContextResolution?.contextFingerprint?.fingerprintHash,\n domainCollision: unifiedState.actionContextResolution?.domainCollision?.detected,\n deployAgentSpiralBlocked:\n unifiedState.actionContextResolution?.actionOverloadingRisk?.deployAgentExample\n ?.spiral_mmo === \"blocked\",\n roboticsActuationEligible:\n unifiedState.actionContextResolution?.roboticsActuationCorrectness?.actuationEligible\n },\n epistemicCoherence: {\n schema: unifiedState.epistemicCoherence?.schema,\n verdict: unifiedState.epistemicCoherence?.systemCoherence?.verdict,\n coherenceScore: unifiedState.epistemicCoherence?.systemCoherence?.score,\n contradictionCount: unifiedState.epistemicCoherence?.crossLayerContradictions?.count,\n operatorDecision: unifiedState.epistemicCoherence?.uxCompression?.operatorDecision,\n headlineTr: unifiedState.epistemicCoherence?.uxCompression?.headline?.tr,\n falseSafetyWarning: Boolean(\n unifiedState.epistemicCoherence?.uxCompression?.falseSafetyWarning\n )\n },\n coherenceAuthorityBoundary: {\n schema: unifiedState.coherenceAuthorityBoundary?.schema,\n bindingValid: unifiedState.coherenceAuthorityBoundary?.eclAuthorityBinding?.valid,\n canExecute: unifiedState.coherenceAuthorityBoundary?.contract?.can_execute,\n mandatoryBannerTr:\n unifiedState.coherenceAuthorityBoundary?.disclaimers?.mandatoryBanner?.tr\n },\n decisionLatencyGovernance: {\n schema: unifiedState.decisionLatencyGovernance?.schema,\n latencyTier: unifiedState.decisionLatencyGovernance?.latencyTier,\n inflationScore: unifiedState.decisionLatencyGovernance?.latencyInflation?.score,\n fastPathEligible: unifiedState.decisionLatencyGovernance?.fastPath?.eligible,\n primaryActionId: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.primaryActionId,\n ackSlaMinutes: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.ackSlaMinutes,\n certaintyCap: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.certaintyCap,\n uncertaintyItems:\n unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.uncertaintyEnvelope?.itemCount\n },\n decisionPacketUncertaintyBoundary: {\n schema: unifiedState.decisionPacketUncertaintyBoundary?.schema,\n invariantValid: unifiedState.decisionPacketUncertaintyBoundary?.invariantCheck?.valid,\n centralInvariant: unifiedState.decisionPacketUncertaintyBoundary?.centralInvariant\n },\n epistemicDecisionPacing: {\n schema: unifiedState.epistemicDecisionPacing?.schema,\n queueSaturationTier:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation?.tier,\n operatorProcessingLatency:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation\n ?.operatorProcessingLatency,\n postureWindowCount:\n unifiedState.epistemicDecisionPacing?.temporaryStaticPostureWindows\n ?.temporaryStaticPostureWindows?.windowCount,\n executionBoundariesValid:\n unifiedState.epistemicDecisionPacing?.executionBoundaries?.valid,\n grantsExecutionApproval: unifiedState.epistemicDecisionPacing?.grantsExecutionApproval\n },\n epistemicTemporalCoherence: {\n schema: unifiedState.epistemicTemporalCoherence?.schema,\n allInvariantsValid: unifiedState.epistemicTemporalCoherence?.allInvariantsValid,\n temporalAlignmentValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.temporalAlignment?.valid,\n crossWindowGuardValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard?.valid,\n delayedTruthValid: unifiedState.epistemicTemporalCoherence?.invariants?.delayedTruthRule?.valid,\n reconciliationRequired:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard\n ?.reconciliation?.required\n },\n realityDriftObserver: {\n schema: unifiedState.realityDriftObserver?.schema,\n driftActive: unifiedState.realityDriftObserver?.driftObserverSummary?.active,\n misapprehensionShapeCount:\n unifiedState.realityDriftObserver?.misapprehensionShapeCatalog?.shapeCount,\n criticalShapeCount:\n unifiedState.realityDriftObserver?.driftObserverSummary?.criticalShapeCount,\n roboticsMismatchActive:\n unifiedState.realityDriftObserver?.roboticsFeedbackMismatch?.active\n },\n driftCausality: {\n schema: unifiedState.driftCausality?.schema,\n shapesWithCausalExplanation: unifiedState.driftCausality?.rdolLinkage?.shapesWithCausalExplanation,\n activeDomainCount: unifiedState.driftCausality?.causalitySummary?.activeDomainCount,\n propagationCausalityActive: unifiedState.driftCausality?.causalDomains?.propagation?.active\n },\n productReadiness: unifiedState.productReadiness,\n stateLayers: {\n raw: unifiedState.stateLayers.raw,\n derived: {\n systemState: unifiedState.stateLayers.derived.systemState,\n narrative: unifiedState.stateLayers.derived.narrative,\n validation: unifiedState.stateLayers.derived.validation\n },\n policy: {\n binding: unifiedState.stateLayers.policy.binding,\n decisionOwner: unifiedState.stateLayers.policy.decisionOwner,\n prohibitedActions: unifiedState.stateLayers.policy.prohibitedActions\n }\n },\n systemState: unifiedState.systemState,\n interpretation: unifiedState.interpretation,\n validation: {\n trustPosture: unifiedState.validation.trustPosture,\n adjustedConfidence: unifiedState.validation.adjustedConfidence,\n agreement: unifiedState.validation.confidenceDecomposition.agreement,\n divergenceScore: unifiedState.validation.divergence.divergenceScore,\n validated: unifiedState.validation.divergence.validated,\n uncertaintyTags: unifiedState.validation.uncertainty.tags.map((t) => t.id),\n flags: unifiedState.validation.divergence.flags.map((f) => f.id)\n },\n compression: unifiedState.compression\n }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 6 * 1024 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (\n !checkHttpRateLimit(`rhizoh_voice_transcribe:${rlKey}`, RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN, 60_000)\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const voiceEnv = rhizohVoiceTranscribeEnvV3();\n if (!voiceEnv.any) {\n sendJson(res, 503, {\n ok: false,\n error: \"voice_asr_not_configured\",\n env: voiceEnv\n });\n return;\n }\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n const result = await runRhizohVoiceTranscribeV3(\n applyLanguagePropagationToVoiceBodyV1(payload, langProp)\n );\n sendJson(res, result.ok ? 200 : 422, {\n ...result,\n traceId: String(payload?.traceId || \"\"),\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n atMs: Date.now()\n });\n } catch (error) {\n sendJson(res, 500, {\n ok: false,\n error: \"voice_transcribe_failed\",\n detail: String(error?.message || error),\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(\"/rhizoh/llm/task/\")) {\n try {\n const rawTaskId = decodeURIComponent(pathname.slice(\"/rhizoh/llm/task/\".length).split(\"/\")[0] || \"\");\n const taskId = String(rawTaskId || \"\").toLowerCase() === \"last\" ? getLatestLlmWorkerTaskIdV0() : rawTaskId;\n const snap = taskId ? getLlmWorkerTaskSnapshotV0(taskId) : null;\n if (!snap) {\n sendJson(res, 404, {\n ok: false,\n error: \"llm_task_not_found\",\n taskId: rawTaskId,\n status: \"missing\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n if (snap.status === \"processing\") {\n sendJson(res, 202, {\n ok: true,\n taskId: snap.taskId,\n status: \"processing\",\n createdAt: snap.createdAt,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n // Terminal states (completed/failed) always HTTP 200 — poll must not look like server fault (500).\n sendJson(res, 200, {\n ...(snap.body || { ok: false, error: \"llm_task_empty\", taskId: snap.taskId }),\n taskId: snap.taskId,\n status: snap.status === \"completed\" ? \"completed\" : snap.status,\n terminal: true,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n } catch (error) {\n console.error(\"[llm-task-poll] handler failed:\", error);\n sendJson(res, 200, {\n ok: false,\n error: \"llm_task_poll_handler_failed\",\n detail: String(error?.message || error),\n status: \"failed\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === rhizohRuntime.routes.rhizohLlm) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 128 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n logRhizohHealth(\"gateway_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: String(payload?.traceId || \"\"),\n auth: auth.ok ? \"ok\" : \"anon\",\n ip\n });\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (!checkHttpRateLimit(`rhizoh_llm:${rlKey}`, RL_RHIZOH_LLM_PER_MIN, 60_000)) {\n const rlTaxonomy = classifyStressResponseV0({ code: \"rate_limit_exceeded\" });\n return sendJson(res, 429, {\n ok: false,\n error: \"rate_limit_exceeded\",\n reply: rlTaxonomy.userMessageTr || \"İstek sınırı aşıldı. Kısa süre sonra tekrar deneyin.\",\n stressClass: rlTaxonomy.stressClass,\n responseAction: rlTaxonomy.responseAction,\n stressMatrix: rlTaxonomy.matrix,\n stressInterpretable: rlTaxonomy.interpretable,\n stressConfidence: rlTaxonomy.stressConfidence,\n actionConfidence: rlTaxonomy.actionConfidence,\n actionSoftened: rlTaxonomy.actionSoftened,\n responseActionStrict: rlTaxonomy.responseActionStrict,\n actionInterpretable: rlTaxonomy.actionInterpretable,\n conflictResolution: rlTaxonomy.conflictResolution,\n directive: \"NONE\"\n });\n }\n\n const requireExplicit = process.env.CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE === \"1\";\n const keyMode = normalizeClientLlmKeySource(payload?.llmKeySource ?? payload?.keySource);\n if (keyMode === null) {\n return sendJson(res, 400, {\n ok: false,\n error: \"invalid_llm_key_source\",\n reply: \"llmKeySource geçersiz. Kullanın: env | user_connection | auto\",\n directive: \"NONE\"\n });\n }\n if (requireExplicit && keyMode === \"auto\") {\n return sendJson(res, 400, {\n ok: false,\n error: \"llm_key_source_required\",\n reply: \"Sunucu CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE=1: istekte llmKeySource (env veya user_connection) zorunlu.\",\n directive: \"NONE\"\n });\n }\n\n const conn = auth.ok ? await resolveConnection(auth.uid, String(payload?.connectionId || \"\")) : null;\n\n if (keyMode === \"user_connection\") {\n if (!auth.ok) {\n return sendJson(res, 401, {\n ok: false,\n error: \"llm_auth_required_for_user_connection\",\n reply: \"Kayıtlı LLM bağlantısı için giriş gerekli.\",\n directive: \"NONE\"\n });\n }\n if (!conn?.apiKey) {\n return sendJson(res, 400, {\n ok: false,\n error: \"user_llm_connection_required\",\n reply: \"Geçerli bir LLM bağlantısı yok; Studio’da bağlantı ekleyin veya llmKeySource=env kullanın.\",\n directive: \"NONE\"\n });\n }\n }\n\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n let safePayload = applyLanguagePropagationToLlmPayloadV1({ ...(payload || {}) }, langProp);\n const trimOut = trimRhizohLlmGatewayPayloadV1(safePayload);\n if (trimOut.meta.trimmed) {\n logRhizohHealth(\"llm_payload_trimmed\", {\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n bytesBefore: trimOut.meta.bytesBefore,\n bytesAfter: trimOut.meta.bytesAfter\n });\n }\n safePayload = trimOut.payload;\n delete safePayload.apiKey;\n delete safePayload.llmKeySource;\n delete safePayload.keySource;\n\n const llmMessage = String(\n safePayload?.message || safePayload?.text || payload?.message || payload?.text || \"\"\n ).trim();\n if (!llmMessage) {\n return sendJson(res, 400, {\n ok: false,\n error: \"message_required\",\n reply: \"Mesaj metni gerekli (message alanı boş).\",\n directive: \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n if (!safePayload.message) {\n safePayload = { ...safePayload, message: llmMessage };\n }\n\n let resolvedProvider = payload?.provider;\n let resolvedModel = payload?.model;\n let connApiKey = \"\";\n\n ({\n resolvedProvider,\n resolvedModel,\n connApiKey\n } = resolveServerLlmProviderV0(keyMode, conn, payload));\n\n const turnInput = {\n safePayload,\n auth,\n keyMode,\n conn,\n resolvedProvider,\n resolvedModel,\n connApiKey\n };\n const turnMeta = {\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n langEcho: buildRhizohLanguagePropagationEchoV1(langProp)\n };\n\n if (\n isLlmWorkerEnabledV0() &&\n isLlmWorkerAsyncEnabledV0() &&\n String(req.headers[\"x-rhizoh-llm-sync\"] || \"\").trim() !== \"1\"\n ) {\n const taskId = enqueueLlmWorkerTaskAsyncV0(turnInput, turnMeta);\n logRhizohHealth(\"llm_worker_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n taskId,\n traceId: String(payload?.traceId || taskId),\n keyMode,\n async: true\n });\n sendJson(res, 202, {\n ok: true,\n taskId,\n status: \"processing\",\n traceId: String(payload?.traceId || taskId),\n pollPath: `/rhizoh/llm/task/${taskId}`,\n gatewayPid: process.pid,\n ...turnMeta.langEcho\n });\n return;\n }\n\n const { result, traceId, turnLatencyMs, spinePhases, sampledTrace } = isLlmWorkerEnabledV0()\n ? await runLlmWorkerTaskV0(turnInput, turnMeta)\n : await rhizohGatewayTurn(turnInput);\n logRhizohHealth(\"llm_response\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId,\n turnLatencyMs: Number(turnLatencyMs || 0),\n keyMode\n });\n sendJson(res, 200, {\n ...result,\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n traceId,\n turnLatencyMs,\n ...(spinePhases ? { spinePhases } : {}),\n ...(sampledTrace !== undefined ? { sampledTrace } : {})\n });\n } catch (error) {\n const msg = String(error?.message || \"\");\n const code = error?.code || \"\";\n let status = 500;\n if (msg === \"payload_too_large\") status = 413;\n else if (msg === \"rate_limit_exceeded\") status = 429;\n else if (\n code === \"cost_hard_limit\" ||\n code === \"phased_rollout_capacity\" ||\n error?.containment === true ||\n String(code).startsWith(\"agent_\")\n ) {\n status = 429;\n } else if (code === \"user_soft_blocked\" || code === \"prompt_abuse_detected\") status = 403;\n else if (\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(code) ||\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(msg) ||\n msg.includes(\"missing_api_key_for_\")\n )\n status = 400;\n let rhizohFailureKind = \"provider_error\";\n if (msg === \"rate_limit_exceeded\" || status === 429) rhizohFailureKind = \"rate_limit\";\n else if (status === 403) rhizohFailureKind = \"policy_block\";\n else if (status === 400 || status === 413) rhizohFailureKind = \"client_config\";\n else if (msg.startsWith(\"provider_http_\")) {\n const st = Number(msg.replace(\"provider_http_\", \"\"));\n if (st === 408 || st === 504) rhizohFailureKind = \"timeout\";\n else rhizohFailureKind = \"provider_error\";\n } else if (/timeout|timed out|ETIMEDOUT/i.test(msg)) rhizohFailureKind = \"timeout\";\n\n const providerHttpMatch = msg.match(/^provider_http_(\\d{3})$/);\n const stressTaxonomy =\n error?.stressClass != null\n ? {\n stressClass: error.stressClass,\n responseAction: error.responseAction,\n stressMatrix: error.stressMatrix,\n stressInterpretable: error.stressInterpretable !== false,\n stressConfidence: error.stressConfidence,\n actionConfidence: error.actionConfidence,\n actionSoftened: error.actionSoftened,\n responseActionStrict: error.responseActionStrict,\n actionInterpretable: error.actionInterpretable,\n conflictResolution: error.conflictResolution,\n stressSecondary: error.stressSecondary,\n responseActions: error.responseActions\n }\n : (() => {\n const t = classifyStressResponseV0({\n code: code || msg,\n rhizohFailureKind,\n providerHttpStatus: providerHttpMatch\n ? Number(providerHttpMatch[1])\n : null\n });\n return {\n stressClass: t.stressClass,\n responseAction: t.responseAction,\n stressMatrix: t.matrix,\n stressInterpretable: t.interpretable,\n stressConfidence: t.stressConfidence,\n actionConfidence: t.actionConfidence,\n actionSoftened: t.actionSoftened,\n responseActionStrict: t.responseActionStrict,\n actionInterpretable: t.actionInterpretable,\n conflictResolution: t.conflictResolution,\n stressSecondary: t.stressSecondary,\n responseActions: t.responseActions\n };\n })();\n sendJson(res, status, {\n ok: false,\n rhizohFailureKind,\n ...(providerHttpMatch ? { providerHttpStatus: Number(providerHttpMatch[1]) } : {}),\n error: code || msg || \"rhizoh_llm_failed\",\n detail: msg || code || undefined,\n reply:\n error?.reply ||\n (status === 500 ? \"Rhizoh bağlantısı geçici olarak kesildi.\" : msg),\n directive: error?.directive || \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n ...stressTaxonomy\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicSeal) {\n try {\n if (!EPISTEMIC_SEAL_SECRET) {\n sendJson(res, 503, { ok: false, error: \"epistemic_seal_disabled\" });\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`epistemic_seal:${ip}`, RL_EPISTEMIC_SEAL_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const truth = body.truth_contract;\n if (!truth || typeof truth !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"truth_contract_required\" });\n return;\n }\n const sealBody = {\n truth_contract: truth,\n runtime_hash: String(body.runtime_hash || \"\"),\n model_route: {\n provider: body.model_route?.provider ?? null,\n model: body.model_route?.model ?? null\n },\n memory_digest: String(body.memory_digest || \"\"),\n world_snapshot_hash: String(body.world_snapshot_hash || \"\"),\n timestamp: Number(body.timestamp) || Date.now()\n };\n const canonical = canonicalEpistemicSealString(sealBody);\n const { hash, signature } = hashAndSignEpistemicSeal(canonical, EPISTEMIC_SEAL_SECRET);\n recordGenesisEpistemicSealIssued(hash);\n const attestation = {\n schema: EPISTEMIC_SEAL_SCHEMA,\n algorithm: \"SHA-256+HMAC-SHA256\",\n keyHint: process.env.CASTLE_EPISTEMIC_SEAL_SECRET ? \"CASTLE_EPISTEMIC_SEAL_SECRET\" : \"CASTLE_GATEWAY_TOKEN\",\n issuedAt: Date.now(),\n gateway: { service: \"castle-gateway\", listenPort: PORT }\n };\n sendJson(res, 200, {\n ok: true,\n hash,\n signature,\n attestation,\n canonicalBytes: Buffer.byteLength(canonical, \"utf8\")\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"seal_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicLogsBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`epistemic_logs:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 120) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const persisted = await persistEpistemicLedgerBatch(auth.uid, entries);\n recordGenesisEpistemicLedgerPersisted(persisted.persisted);\n // Internal cognition layer: forecast stays gateway/ledger-only (no client exposure).\n try {\n await persistEpistemicForecastBatch(auth.uid, persisted.normalized || []);\n } catch {\n /* forecast failures must not break primary ledger ingest */\n }\n sendJson(res, 200, {\n ok: true,\n persisted: persisted.persisted,\n mode: persisted.mode,\n latest: persisted.normalized?.[persisted.normalized.length - 1] || null\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"epistemic_log_ingest_failed\") });\n }\n return;\n }\n\n sendJson(res, 404, { ok: false, error: \"not_found\" });\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\nspiralWssForGenesis = wss;\nconst orchestrator = createOrchestrator();\nconst clientStats = new Map();\nlet broadcasterClientId = null;\nconst spiralState = {\n rooms: [{ id: \"room-default\", name: \"Spiral Default Arena\" }],\n activeByClient: new Map(),\n characters: new Map() // id -> {id,name,role,roomId,source}\n};\n\n/** Castle social rooms — keyed by `castleRoomKey` from {@link WS_MESSAGE.CASTLE_SOCIAL_PULSE}. */\nconst castleSocialRoomState = new Map();\nconst CASTLE_SOCIAL_MEMBER_TTL_MS = 90_000;\nconst CASTLE_SOCIAL_MAX_PULSE_BYTES = 8192;\nconst RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES = 5 * 1024 * 1024;\nconst RHIZOH_VOICE_LIVE_MAX_CHUNKS = 96;\n\n/**\n * Voice Engine v3 live media lane.\n * Client streams MediaRecorder chunks over the existing gateway WS; STOP reuses the\n * gateway ASR runner so orchestration remains the single authority downstream.\n */\nconst rhizohVoiceLiveSessions = new WeakMap();\n\nfunction appendRhizohVoiceLiveEventV0(session, type, payloadRefSeed = \"\") {\n if (!session) return null;\n session.eventSeq = (Number(session.eventSeq) || 0) + 1;\n const event = createRhizohImmutableEventV0({\n type,\n sessionId: session.sessionId,\n traceId: session.traceId,\n actorSeed: session.sessionId || session.traceId,\n eventSeq: session.eventSeq,\n payloadRef: createRhizohPayloadRefV0(payloadRefSeed || `${session.sessionId}:${type}:${session.eventSeq}`)\n });\n session.events = [...(session.events || []), event].slice(-64);\n return event;\n}\n\nfunction sendRhizohVoiceLiveEnvelopeV0(socket, type, payload) {\n try {\n if (socket?.readyState === 1) socket.send(JSON.stringify(createEnvelope(type, payload)));\n } catch {\n /* noop */\n }\n}\n\nfunction decodeRhizohVoiceLiveChunkV0(payload = {}) {\n const b64 = String(payload.audioBase64 || \"\").trim();\n if (!b64) return { ok: false, error: \"audio_base64_required\" };\n try {\n const buf = Buffer.from(b64, \"base64\");\n if (!buf.length) return { ok: false, error: \"audio_decode_empty\" };\n return { ok: true, buffer: buf };\n } catch {\n return { ok: false, error: \"audio_base64_invalid\" };\n }\n}\n\nfunction handleRhizohVoiceLiveStartV0(socket, payload = {}) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const sessionId = String(p.sessionId || `live_${Date.now().toString(36)}`).slice(0, 96);\n rhizohVoiceLiveSessions.set(socket, {\n sessionId,\n traceId: String(p.traceId || \"\").slice(0, 128),\n mimeType: String(p.mimeType || \"audio/webm\").slice(0, 80),\n languageCode: String(p.languageCode || \"tr-TR\").slice(0, 32),\n path: String(p.path || \"both\").slice(0, 24),\n chunks: [],\n chunkIndexes: [],\n totalBytes: 0,\n nextChunkIndex: 1,\n eventSeq: 0,\n events: [],\n startedAtMs: Date.now()\n });\n appendRhizohVoiceLiveEventV0(rhizohVoiceLiveSessions.get(socket), \"VOICE_LIVE_SESSION_STARTED\", `${sessionId}:live_session`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_START, {\n ok: true,\n sessionId,\n schema: \"castle.rhizoh.voice_live.gateway_lane.v0\"\n });\n}\n\nfunction handleRhizohVoiceLiveChunkV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const sequence = validateRhizohVoiceLiveChunkPayloadV0(payload, {\n expectedChunkIndex: session.nextChunkIndex\n });\n if (!sequence.ok) {\n rhizohVoiceLiveSessions.delete(socket);\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:chunk_reject`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: sequence.error,\n expectedChunkIndex: sequence.expectedChunkIndex,\n chunkIndex: sequence.chunkIndex\n });\n return;\n }\n const decoded = decodeRhizohVoiceLiveChunkV0(payload);\n if (!decoded.ok) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: decoded.error\n });\n return;\n }\n if (\n session.chunks.length >= RHIZOH_VOICE_LIVE_MAX_CHUNKS ||\n session.totalBytes + decoded.buffer.length > RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES\n ) {\n rhizohVoiceLiveSessions.delete(socket);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_audio_too_large\"\n });\n return;\n }\n session.chunks.push(decoded.buffer);\n session.chunkIndexes.push(sequence.chunkIndex);\n session.totalBytes += decoded.buffer.length;\n session.nextChunkIndex += 1;\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_CHUNK_ACCEPTED\", `${session.sessionId}:chunk:${sequence.chunkIndex}`);\n}\n\nfunction handleRhizohVoiceLiveStopV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n rhizohVoiceLiveSessions.delete(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const lastChunkIndex = Number(payload?.lastChunkIndex);\n if (Number.isInteger(lastChunkIndex) && lastChunkIndex !== session.chunks.length) {\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:stop_mismatch`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_chunk_count_mismatch\",\n expectedChunkCount: lastChunkIndex,\n receivedChunkCount: session.chunks.length\n });\n return;\n }\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_STOP_RECEIVED\", `${session.sessionId}:stop:${session.chunks.length}`);\n const audio = Buffer.concat(session.chunks);\n if (!audio.length) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"audio_empty\"\n });\n return;\n }\n runRhizohVoiceTranscribeV3({\n path: String(payload?.path || session.path || \"both\"),\n audioBase64: audio.toString(\"base64\"),\n mimeType: String(payload?.mimeType || session.mimeType || \"audio/webm\"),\n languageCode: String(payload?.languageCode || session.languageCode || \"tr-TR\"),\n traceId: String(payload?.traceId || session.traceId || \"\"),\n sessionId: session.sessionId,\n transport: \"gateway_ws_live\"\n })\n .then((result) => {\n appendRhizohVoiceLiveEventV0(session, result?.ok ? \"VOICE_TRANSCRIBE_FINAL\" : \"VOICE_TRANSCRIBE_FAILED\", `${session.sessionId}:final`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_FINAL, {\n ...result,\n sessionId: session.sessionId,\n transportPath: \"gateway_ws_live\",\n bytes: audio.length,\n chunkCount: session.chunks.length,\n chunkSequence: Object.freeze({\n ok: true,\n firstChunkIndex: session.chunkIndexes[0] || null,\n lastChunkIndex: session.chunkIndexes[session.chunkIndexes.length - 1] || null,\n receivedChunkCount: session.chunks.length\n }),\n eventTrace: Object.freeze((session.events || []).slice(-16)),\n recordedMs: Math.max(0, Date.now() - session.startedAtMs)\n });\n })\n .catch((e) => {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_transcribe_failed\",\n detail: String(e?.message || e)\n });\n });\n}\n\nfunction removeCastleSocialClientFromAllRooms(clientId) {\n const cid = String(clientId || \"\");\n if (!cid) return;\n for (const room of castleSocialRoomState.values()) {\n room.members.delete(cid);\n }\n}\n\nfunction buildCastleSocialRoomPayload(roomKey) {\n const room = castleSocialRoomState.get(roomKey);\n if (!room) return null;\n const now = Date.now();\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const roster = [...room.members.values()].map((m) => ({\n userId: m.userId,\n lastMs: m.lastMs,\n ...m.sp\n }));\n const peerFeeds =\n room.walPeers && room.walPeers.size\n ? [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }))\n : [];\n return { castleRoomKey: roomKey, seq: room.seq, roster, peerFeeds, ts: now };\n}\n\nconst CASTLE_WAL_PEER_MAX_FEED_BYTES = 48 * 1024;\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {{ ok: boolean, code?: string, reason?: string }} gate\n */\nfunction rejectWalPeerIngressV0(socket, gate) {\n const code = gate.code || \"wal_peer_rejected\";\n recordWalPeerRejectV0(code);\n recordSubstrateOtelEventV0(\"wal_peer_reject\", { code, reason: String(gate.reason || \"\").slice(0, 120) });\n socket.send(\n JSON.stringify(\n createEnvelope(WS_MESSAGE.ERROR, {\n error: gate.reason || \"WAL peer ingress rejected.\",\n code\n })\n )\n );\n}\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {Record<string, unknown> | null | undefined} walPeerFeed\n * @returns {boolean}\n */\nfunction gateWalPeerIngressV0(socket, walPeerFeed) {\n const authGate = assertWalPeerSocketAuthorityV0(socket);\n if (!authGate.ok) {\n rejectWalPeerIngressV0(socket, authGate);\n return false;\n }\n const sigGate = validateWalPeerFeedSignatureV0(\n walPeerFeed && typeof walPeerFeed === \"object\" ? walPeerFeed : {}\n );\n if (!sigGate.ok) {\n rejectWalPeerIngressV0(socket, sigGate);\n return false;\n }\n return true;\n}\n\nfunction handleCastleWalPeerFeed(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_WAL_PEER_MAX_FEED_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_WAL_PEER_FEED too large.\" })));\n return;\n }\n const walPeerFeed = p.walPeerFeed && typeof p.walPeerFeed === \"object\" ? p.walPeerFeed : {};\n if (!gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const castleRoomKey = String(p.castleRoomKey || \"wal:convergence\").slice(0, 64);\n const castleId = String(p.castleId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId,\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: room.members.get(socket.clientId)?.sp ?? {}\n });\n const peerFeeds = [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }));\n const body = {\n castleRoomKey,\n seq: room.seq,\n peerFeeds,\n ts: now\n };\n const env = createEnvelope(WS_MESSAGE.CASTLE_WAL_PEER_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys(), ...room.walPeers.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction handleCastleSocialPulse(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_SOCIAL_MAX_PULSE_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_SOCIAL_PULSE too large.\" })));\n return;\n }\n const castleRoomKey = String(p.castleRoomKey || \"default\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n const sp = p.socialPulse && typeof p.socialPulse === \"object\" ? p.socialPulse : {};\n const walPeerFeed =\n p.walPeerFeed && typeof p.walPeerFeed === \"object\"\n ? p.walPeerFeed\n : sp.walPeerFeed && typeof sp.walPeerFeed === \"object\"\n ? sp.walPeerFeed\n : null;\n if (walPeerFeed && !gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const spNext = walPeerFeed ? { ...sp, walPeerFeed } : { ...sp };\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: spNext\n });\n if (walPeerFeed) {\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId: String(p.castleId || userId).slice(0, 64),\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n }\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const body = buildCastleSocialRoomPayload(castleRoomKey);\n if (!body) return;\n const env = createEnvelope(WS_MESSAGE.CASTLE_SOCIAL_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcast(msgObj) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcastState() {\n broadcast(\n createEnvelope(WS_MESSAGE.BROADCAST_STATE, {\n broadcasterClientId,\n viewers: [...wss.clients].filter((c) => c.readyState === 1).length\n })\n );\n}\n\nfunction currentSpiralStatePayload() {\n return {\n rooms: spiralState.rooms,\n characters: [...spiralState.characters.values()]\n };\n}\n\nfunction broadcastSpiralState() {\n broadcast(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload()));\n}\n\nfunction ensureRoom(roomId, fallbackName = null) {\n if (!roomId) return spiralState.rooms[0].id;\n const exists = spiralState.rooms.find((r) => r.id === roomId);\n if (exists) return exists.id;\n const created = { id: roomId, name: fallbackName || `Spiral-${roomId.slice(-4)}` };\n spiralState.rooms.push(created);\n return created.id;\n}\n\nfunction syncSpiralCharactersWithWorld(snapshot) {\n const agents = snapshot?.agents || [];\n for (const a of agents) {\n if (!spiralState.characters.has(a.id)) {\n spiralState.characters.set(a.id, {\n id: a.id,\n name: a.id,\n role: \"agent\",\n roomId: spiralState.rooms[0].id,\n source: \"world\"\n });\n }\n }\n}\n\nfunction broadcastExcept(msgObj, exceptSocket) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client !== exceptSocket && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction validateInputFrame(payload, socket) {\n const stats = clientStats.get(socket.clientId) || { lastFrame: 0, windowStart: Date.now(), msgCount: 0 };\n const now = Date.now();\n if (now - stats.windowStart > 5000) {\n stats.windowStart = now;\n stats.msgCount = 0;\n }\n stats.msgCount += 1;\n clientStats.set(socket.clientId, stats);\n if (stats.msgCount > 120) return \"Rate limit exceeded.\";\n\n if (!payload || !Number.isInteger(payload.frame)) return \"frame must be integer.\";\n if (payload.frame <= stats.lastFrame) return \"frame must be monotonic.\";\n const commands = Array.isArray(payload.commands) ? payload.commands : [];\n if (commands.length > 8) return \"Too many commands in one frame.\";\n for (const cmd of commands) {\n if (!cmd || typeof cmd.command !== \"string\") return \"Invalid command format.\";\n if (\n cmd.command !== \"SPAWN_AGENT\" &&\n cmd.command !== \"LIST_CASTLES\" &&\n cmd.command !== \"SPAWN_ENTITY\" &&\n cmd.command !== \"CREATE_CASTLE\"\n ) return \"Unsupported command.\";\n }\n stats.lastFrame = payload.frame;\n clientStats.set(socket.clientId, stats);\n return null;\n}\n\nfunction validateSignalPayload(payload) {\n if (!payload || typeof payload !== \"object\") return \"Invalid signaling payload.\";\n const signalType = payload.signalType;\n if (Object.values(CASTLE_NETWORK_SIGNAL_V0).includes(signalType)) {\n return null;\n }\n if (![\"OFFER\", \"ANSWER\", \"ICE\"].includes(signalType)) return \"Invalid signal type.\";\n if (typeof payload.to !== \"string\" || payload.to.length < 3) return \"Invalid signal destination.\";\n if (signalType !== \"ICE\" && JSON.stringify(payload.sdp || {}).length > 16 * 1024) return \"SDP too large.\";\n if (signalType === \"ICE\" && JSON.stringify(payload.candidate || {}).length > 6 * 1024) return \"ICE too large.\";\n return null;\n}\n\nfunction sanitizeRawMessage(raw) {\n if (!raw) return null;\n const text = raw.toString();\n if (Buffer.byteLength(text, \"utf8\") > MAX_MESSAGE_BYTES) return null;\n return text;\n}\n\nwss.on(\"connection\", async (socket, req) => {\n const origin = req?.headers?.origin || \"\";\n if (!httpCors.isWebSocketOriginAllowed(origin)) {\n socket.close(1008, \"Origin not allowed\");\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const path = req?.url || \"\";\n const query = new URL(`ws://localhost${path}`).searchParams;\n const token = query.get(\"token\");\n if (!token || token !== REQUIRED_GATEWAY_TOKEN) {\n socket.close(1008, \"Invalid gateway token\");\n return;\n }\n }\n if (REQUIRE_AUTH) {\n const result = await verifyClientToken(req);\n if (!result.ok) {\n const isLocal = origin.includes(\"localhost\") || origin.includes(\"127.0.0.1\") || !origin;\n if (!(ALLOW_DEV_ANON && isLocal)) {\n socket.close(1008, result.reason);\n return;\n }\n socket.auth = { ok: true, kind: \"dev-anon\", user: { uid: \"dev-anon\" } };\n } else {\n socket.auth = result;\n }\n }\n\n socket.clientId = `c-${Math.random().toString(36).slice(2, 8)}`;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.HELLO, { message: \"Castle Gateway online.\", clientId: socket.clientId })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.WORLD_SNAPSHOT, orchestrator.worldSnapshot())));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.BROADCAST_STATE, { broadcasterClientId, viewers: [...wss.clients].filter((c) => c.readyState === 1).length })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload())));\n broadcastExcept(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }), socket);\n\n socket.on(\"message\", (raw) => {\n const safeRaw = sanitizeRawMessage(raw);\n if (!safeRaw) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Message too large or invalid.\" })));\n return;\n }\n const parsed = safeJsonParse(safeRaw);\n if (!parsed?.type) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid message.\" })));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND_TEXT) {\n const canonical = parseTextToCommand(parsed.payload?.text);\n if (!canonical) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Unknown command text.\" })));\n return;\n }\n const result = orchestrator.applyCommand(canonical);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND) {\n const result = orchestrator.applyCommand(parsed.payload);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n }\n\n if (parsed.type === WS_MESSAGE.INPUT_FRAME) {\n const err = validateInputFrame(parsed.payload, socket);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const frame = parsed.payload.frame;\n const commands = Array.isArray(parsed.payload?.commands) ? parsed.payload.commands : [];\n orchestrator.queueInputFrame(socket.clientId, frame, commands);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, queued: true, frame })));\n }\n\n if (parsed.type === WS_MESSAGE.SIGNAL) {\n const payload = parsed.payload || {};\n if (Object.values(CASTLE_NETWORK_SIGNAL_V0).includes(payload.signalType)) {\n const netErr = validateCastleNetworkSignalPayloadV0(socket, payload);\n if (netErr) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: netErr })));\n return;\n }\n handleCastleNetworkSignalV0(socket, payload, wss);\n return;\n }\n const err = validateSignalPayload(payload);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const signalType = payload.signalType;\n const to = payload.to;\n for (const client of wss.clients) {\n if (client.clientId === to && client.readyState === 1) {\n client.send(JSON.stringify(createEnvelope(WS_MESSAGE.SIGNAL, { ...payload, from: socket.clientId })));\n break;\n }\n }\n }\n\n if (parsed.type === WS_MESSAGE.DRONE_TELEMETRY) {\n const payload = parsed.payload || {};\n const json = JSON.stringify(payload);\n if (Buffer.byteLength(json, \"utf8\") > 48 * 1024) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"DRONE_TELEMETRY payload too large.\" })));\n return;\n }\n broadcastExcept(createEnvelope(WS_MESSAGE.DRONE_TELEMETRY, { ...payload, relayFrom: socket.clientId }), socket);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_START) {\n handleRhizohVoiceLiveStartV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_CHUNK) {\n handleRhizohVoiceLiveChunkV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_STOP) {\n handleRhizohVoiceLiveStopV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_SOCIAL_PULSE) {\n handleCastleSocialPulse(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_WAL_PEER_FEED) {\n handleCastleWalPeerFeed(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.OPEN_DATA_QUERY) {\n queryOpenData(parsed.payload)\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: true, result })));\n })\n .catch((e) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: false, error: e.message })));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_QUERY) {\n runRhizohBrainV2({\n ...(parsed.payload || {}),\n uid: socket?.auth?.user?.uid || \"unknown\"\n })\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, result)));\n })\n .catch(async () => {\n const fallback = await runRhizohBrain(parsed.payload || {}).catch(() => ({\n ok: false,\n reply: \"Rhizoh temporary disturbance. Habitat remains stable.\"\n }));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, fallback)));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.BROADCAST_REGISTER) {\n const role = String(parsed.payload?.role || \"\");\n if (role === \"GENESIS_BROADCAST_AGENT\") {\n broadcasterClientId = socket.clientId;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, registered: role })));\n broadcastState();\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.STUDIO_CUE) {\n // Only registered broadcaster can push global cues.\n if (!broadcasterClientId || socket.clientId !== broadcasterClientId) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Only broadcaster agent can emit STUDIO_CUE.\" })));\n return;\n }\n const cue = parsed.payload || {};\n if (typeof cue !== \"object\") {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid STUDIO_CUE payload.\" })));\n return;\n }\n broadcast(createEnvelope(WS_MESSAGE.STUDIO_CUE, cue));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_JOIN_ROOM) {\n const requested = String(parsed.payload?.roomId || \"\").trim();\n const requestedName = String(parsed.payload?.roomName || \"\").trim();\n const roomId = ensureRoom(requested || `room-${Math.random().toString(36).slice(2, 7)}`, requestedName || null);\n spiralState.activeByClient.set(socket.clientId, roomId);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, roomId })));\n broadcastSpiralState();\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_CREATE_CHARACTER) {\n const name = String(parsed.payload?.name || \"\").trim().slice(0, 32) || `AI-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;\n const role = String(parsed.payload?.role || \"agent\").trim().slice(0, 24) || \"agent\";\n const roomId = ensureRoom(\n String(parsed.payload?.roomId || spiralState.activeByClient.get(socket.clientId) || spiralState.rooms[0].id),\n null\n );\n\n // Character creation always maps to an authoritative world agent.\n const result = orchestrator.applyCommand({\n command: COMMAND.SPAWN_AGENT,\n params: { targetCastleId: \"castle-01\" }\n });\n if (!result?.ok || !result?.data?.id) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Character creation failed.\" })));\n return;\n }\n const id = result.data.id;\n spiralState.characters.set(id, { id, name, role, roomId, source: \"spawned\" });\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, characterId: id, roomId })));\n broadcastSpiralState();\n return;\n }\n });\n\n socket.on(\"close\", () => {\n if (socket.clientId === broadcasterClientId) broadcasterClientId = null;\n clientStats.delete(socket.clientId);\n rhizohVoiceLiveSessions.delete(socket);\n spiralState.activeByClient.delete(socket.clientId);\n removeCastleSocialClientFromAllRooms(socket.clientId);\n removeCastleNetworkClientV0(socket.clientId, wss);\n broadcast(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }));\n broadcastState();\n });\n});\n\nsetInterval(() => {\n void (async () => {\n try {\n const { frame, snapshot, delta, packedDelta } = orchestrator.tick(1 / 20);\n syncSpiralCharactersWithWorld(snapshot);\n broadcast(createEnvelope(WS_MESSAGE.NETWORK_TICK, { frame }));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_TICK, snapshot));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA, delta));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA_PACKED, packedDelta));\n broadcastSpiralState();\n await runAcademyEventTick();\n const executed = await tickAutomations(Date.now());\n for (const a of executed) {\n await addTranscript(a.uid, {\n source: \"planner\",\n eventType: \"automation-executed\",\n text: `Automation executed: ${a.title} (${a.action})`,\n roomId: \"studio-main\",\n meta: { automationId: a.id, action: a.action, deviceId: a.deviceId || \"\" }\n });\n }\n } catch (e) {\n console.error(\"[GATEWAY] world tick / automations loop error:\", String(e?.message || e));\n }\n })();\n}, 50);\n\n(async () => {\n await rhizohRuntime.telemetry.initOpenTelemetry();\n if (String(process.env.CASTLE_GENESIS_DISK_PERSIST ?? \"\").trim() === \"\") {\n process.env.CASTLE_GENESIS_DISK_PERSIST = \"1\";\n }\n const genesisDiskBoot = await hydrateGenesisContinuityPersistenceBootV0();\n if (!genesisDiskBoot.ok) {\n console.warn(\"[GATEWAY] genesis disk hydrate failed; continuing with in-memory genesis continuity\");\n }\n httpServer.listen(PORT, () => {\n startGenesisCanonicalClock();\n installGenesisCheckpointSurfaceGetter(buildGenesisRuntimeSurfacePayloadLive);\n setGenesisContinuityAfterPublishHook((seq) => {\n noteGenesisCheckpointSeqCommitted(seq);\n noteGenesisContinuitySeqAcceptedV0(seq);\n });\n startGenesisContinuityInfraSampler(buildGenesisRuntimeSurfacePayloadLive, 2000);\n logProductionObservatorySurfaceGuardsV0();\n logGatewaySubstrateAuthorityGuardsV0();\n if (isLlmWorkerEnabledV0()) {\n try {\n const workerBoot = initLlmWorkerAtBootV0();\n console.log(\"[GATEWAY] llm worker boot:\", workerBoot);\n } catch (workerErr) {\n console.error(\"[GATEWAY] llm worker boot failed:\", workerErr);\n }\n }\n console.log(`[GATEWAY] ws/http://localhost:${PORT}`);\n console.log(\n `[GATEWAY] genesis continuity: GET ${rhizohRuntime.routes.genesisRuntime} | POST ${rhizohRuntime.routes.genesisIngress} | SSE ${rhizohRuntime.routes.genesisStream} | replay ${rhizohRuntime.routes.genesisReplay} | diff ${rhizohRuntime.routes.genesisReplayDiff} | equiv ${rhizohRuntime.routes.genesisReplayEquivalence} | analytics ${rhizohRuntime.routes.genesisReplayAnalytics} | evolution ${rhizohRuntime.routes.genesisReplayEvolution}`\n );\n if (String(process.env.CASTLE_GENESIS_OBSERVATORY_LIVE_V01 ?? \"\").trim() === \"1\") {\n console.log(\"GENESIS OBSERVATORY LIVE v0.1 — READ ONLY PHASE STARTED\");\n }\n });\n})().catch((e) => {\n console.error(\"[GATEWAY] fatal boot (before or during listen):\", e);\n process.exit(1);\n});\n"}, "dc752236324b867ec09a95c6fc46348f02007c78": {"t": 1785754137.8215961, "n": 170683, "h": "9b2759cda70e5358", "c": "import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport dotenv from \"dotenv\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconsole.log(\"🔥 [GENESIS_BOOT] entry file:\", __filename);\nconsole.log(\"🔥 [GENESIS_BOOT] node env:\", process.env.NODE_ENV);\nconsole.log(\"🔥 [GENESIS_BOOT] pid:\", process.pid);\nconsole.log(\"🧬 [GENESIS_BOOT] cwd:\", process.cwd());\nconsole.log(\"🧬 [GENESIS_BOOT] import.meta.url:\", import.meta.url);\ndotenv.config({ path: path.join(__dirname, \"..\", \".env\") });\ndotenv.config({ path: path.join(__dirname, \"..\", \".env.local\"), override: true });\n\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport { createServer } from \"node:http\";\nimport { createOrchestrator } from \"@castle/orchestrator\";\nimport { parseTextToCommand } from \"@castle/command-dsl\";\nimport {\n WS_MESSAGE,\n COMMAND,\n createEnvelope,\n createRhizohImmutableEventV0,\n createRhizohPayloadRefV0,\n safeJsonParse,\n validateRhizohVoiceLiveChunkPayloadV0\n} from \"@castle/protocol\";\nimport { queryOpenData } from \"./openData.js\";\nimport { verifyClientToken } from \"./auth.js\";\nimport { runRhizohBrain } from \"./rhizohBrain.js\";\nimport { queryRhizohLlm } from \"./rhizohLlmGateway.js\";\nimport { rhizohGatewayTurn } from \"./rhizohGatewayTurn.js\";\nimport {\n enqueueLlmWorkerTaskAsyncV0,\n getLatestLlmWorkerTaskIdV0,\n getLlmWorkerDebugSnapshotV0,\n getLlmWorkerTaskSnapshotV0,\n initLlmWorkerAtBootV0,\n isLlmWorkerAsyncEnabledV0,\n isLlmWorkerEnabledV0,\n runLlmWorkerTaskV0\n} from \"./llmWorkerRuntime.js\";\nimport {\n RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n runRhizohVoiceTranscribeV3,\n rhizohVoiceTranscribeEnvV3\n} from \"./rhizohVoiceTranscribeV3.js\";\nimport { createHttpCorsPolicy, normalizeHttpOrigin, parseAllowedOriginsFromEnv } from \"./httpCorsPolicyV1.js\";\nimport {\n applyLanguagePropagationToLlmPayloadV1,\n applyLanguagePropagationToVoiceBodyV1,\n buildRhizohLanguagePropagationEchoV1,\n parseRhizohLanguagePropagationV1\n} from \"./rhizohLanguagePropagationV1.js\";\nimport { trimRhizohLlmGatewayPayloadV1 } from \"./rhizohLlmPayloadTrimV1.js\";\nimport {\n meshAppendDelta,\n meshContinuityAggregate,\n meshJoin,\n meshLeave,\n meshReplayFromNodeId,\n meshReplayFromSeq,\n meshSnapshot,\n meshSubscribeSse\n} from \"./presenceRoomMesh.js\";\nimport { runRhizohBrainV2 } from \"./rhizohBrainV2.js\";\nimport {\n listConnections,\n createConnection,\n updateConnection,\n deleteConnection,\n setDefaultConnection,\n resolveConnection\n} from \"./llmConnectionsStore.js\";\nimport { getCloudSyncSnapshotV0, mergeCloudSyncSnapshotV0 } from \"./castleCloudSyncStore.js\";\nimport { checkHttpRateLimit, getHttpClientIp } from \"./castleHttpRateLimit.js\";\nimport {\n submitAbuseReportV0,\n listModerationQueueV0,\n softBlockUserV0,\n removeSoftBlockV0,\n listSoftBlockedUsersV0\n} from \"./ops/moderationMvpV0.js\";\nimport { listRecentAgentSnapshotsV0 } from \"./ops/agentObservabilityV0.js\";\nimport { readAgentContainmentConfigV0 } from \"./ops/agentContainmentV0.js\";\nimport { readCostContainmentConfigV0, getCostContainmentStatsV0 } from \"./ops/costContainmentV0.js\";\nimport { getGlobalCostLedgerSnapshotV0, getGclBackendStatusV0 } from \"./ops/globalCostLedgerV0.js\";\nimport { evaluateEconomicStrategyL3V0 } from \"./ops/economicStrategyEngineL3V0.js\";\nimport { getPhasedRolloutStatsV0, readPhasedRolloutTierV0 } from \"./ops/phasedRolloutV0.js\";\nimport { buildUnifiedStateNarrativeV0 } from \"./ops/unifiedStateNarrativeV0.js\";\nimport { resolveHardeningTenantIdV0 } from \"./ops/narrativeTenantIsolationV0.js\";\nimport {\n classifyStressResponseV0,\n verifyStressTaxonomyCoverageV0,\n verifyStressConflictResolutionV0\n} from \"./ops/stressResponseTaxonomyV0.js\";\nimport { verifyResolutionStabilityEnvelopeV0 } from \"./ops/resolutionStabilityEnvelopeV0.js\";\nimport { logLlmAccess } from \"./castleLlmAudit.js\";\nimport { appendMemory, listMemories, getMemoryContext, getPersonaGoalMemory, setPersonaGoalMemory, autoCompactMemories } from \"./memoryStore.js\";\nimport { getFirebasePersistence } from \"./firebasePersistence.js\";\nimport { registerAgentIdentity, listAgentIdentities, getAgentIdentity, updateAgentIdentity } from \"./agentIdentityStore.js\";\nimport { queueAcademyEvent, listAcademyEvents, resolveAcademyEvent, runAcademyEventTick } from \"./academyEventEngine.js\";\nimport { enqueuePdfJob, getPdfJob } from \"./pdfPipeline.js\";\nimport {\n createPublishSession,\n listPublishSessions,\n updatePublishSession,\n addTranscript,\n listTranscripts,\n searchMetadata,\n registerDevice,\n listDevices,\n updateDevice,\n appendTelemetry,\n listTelemetry,\n createAutomation,\n listAutomations,\n tickAutomations,\n enqueueCommand,\n listCommands,\n updateCommandStatus,\n registerSocialChannel,\n listSocialChannels\n} from \"./studioOpsStore.js\";\nimport { metrics as infraMetrics } from \"./infra/metrics.js\";\nimport { renderRhizohPrometheusMetrics, rhizohEnterpriseMetrics } from \"./infra/rhizohEnterpriseMetrics.js\";\nimport { scoreHealth } from \"./infra/healthScore.js\";\nimport { getTraceById, listTracesBySession } from \"./infra/traceRegistry.js\";\nimport { canonicalEpistemicSealString, hashAndSignEpistemicSeal, EPISTEMIC_SEAL_SCHEMA } from \"./epistemicSeal.js\";\nimport { persistEpistemicLedgerBatch } from \"./epistemicLedgerStore.js\";\nimport { persistEpistemicForecastBatch } from \"./epistemicForecastStore.js\";\nimport { buildRhizohExternalGroundTruthPayload } from \"./rhizohExternalGroundTruthGateway.js\";\nimport { ingestRhizohExternalLossBatchHttp } from \"./rhizohExternalLossIngestGateway.js\";\nimport {\n ingestRhizohProductOutcomeHttp,\n listRhizohProductOutcomeAggregates,\n verifyRhizohOutcomeHmac,\n verifyRhizohOutcomeSourceToken\n} from \"./rhizohProductOutcomeIngestGateway.js\";\nimport { initRhizoh } from \"./rhizohProductionBootstrap.js\";\nimport { handleAcademicObservatoryExportGetV0 } from \"./rhizoh/academicObservatoryHttpV0.js\";\nimport {\n handleLiveSportsBundleGetV0,\n handleLiveNewsHeadlinesGetV0,\n handleLiveWorldFeedGetV0\n} from \"./rhizoh/liveWorldFeedHttpV0.js\";\nimport {\n buildGenesisRuntimeSurfacePayload,\n recordGenesisEpistemicLedgerPersisted,\n recordGenesisEpistemicSealIssued,\n startGenesisCanonicalClock\n} from \"./genesisRuntimeSurfaceV0.js\";\nimport { registerGenesisSseClient, getGenesisContinuitySeq, setGenesisContinuityAfterPublishHook } from \"./genesisContinuityStreamHubV0.js\";\nimport { computeGenesisReplayFingerprintV0 } from \"./genesisReplayFingerprintV0.js\";\nimport { buildGenesisGatewayCapabilitiesV0 } from \"./genesisGatewayCapabilitiesV0.js\";\nimport {\n installGenesisCheckpointSurfaceGetter,\n noteGenesisCheckpointSeqCommitted,\n getLatestGenesisSignedCheckpoint\n} from \"./genesisContinuityCheckpointV0.js\";\nimport { hydrateGenesisContinuityPersistenceBootV0 } from \"./genesisContinuityHydrateBootV0.js\";\nimport { startGenesisContinuityInfraSampler } from \"./genesisContinuityInfraSamplerV0.js\";\nimport {\n genesisCheckpointQueryBySeqV0,\n genesisCheckpointQueryRangeV0,\n genesisCheckpointQueryLineageV0\n} from \"./genesisCheckpointQueryV0.js\";\nimport {\n genesisContinuityEventArchiveEnabled,\n queryGenesisContinuityEventArchiveV0,\n GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION\n} from \"./genesisContinuityEventArchiveV0.js\";\nimport { resolveGenesisReplayRouterV1, GENESIS_REPLAY_ROUTER_SCHEMA } from \"./genesisReplayRouterV1.js\";\nimport {\n computeGenesisReplayTemporalDiffV1,\n GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA\n} from \"./genesisReplayTemporalDiffV1.js\";\nimport { compareGenesisReplayEquivalenceV1 } from \"./genesisReplayEquivalenceV1.js\";\nimport { computeGenesisReplayAnalyticsV1 } from \"./genesisReplayAnalyticsV1.js\";\nimport { computeGenesisReplayEvolutionV1 } from \"./genesisReplayEvolutionV1.js\";\nimport { handleStripeCheckoutCreate, handleStripeMembershipWebhook } from \"./stripeWebhookMembership.js\";\nimport { buildRealityHealthPayload } from \"./edgeRealityConsistencyV1.js\";\nimport {\n assertWalPeerSocketAuthorityV0,\n buildGatewaySubstrateAuthoritySnapshotV0,\n logGatewaySubstrateAuthorityGuardsV0,\n validateWalPeerFeedSignatureV0\n} from \"./gatewaySubstrateAuthorityV0.js\";\nimport {\n buildSubstrateOperationalSnapshotV0,\n recordWalPeerRejectV0,\n renderSubstratePrometheusMetricsV0\n} from \"./infra/substrateOperationalMetrics.js\";\nimport { ingestClientSubstrateHealthV0 } from \"./rhizohSubstrateHealthIngestV0.js\";\nimport { recordSubstrateOtelEventV0 } from \"./infra/opentelemetrySubstrateV0.js\";\nimport {\n GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n getGenesisWorldObservationIngressStatusV0,\n ingestGenesisWorldObservationV0\n} from \"./genesisContinuityClientIngressV0.js\";\nimport {\n getGenesisContinuitySeqAuditV0,\n noteGenesisContinuitySeqAcceptedV0\n} from \"./genesisContinuitySeqGapV0.js\";\n\n/** Render / Fly / Railway: `PORT` — yoksa `CASTLE_GATEWAY_PORT` — yoksa 8090. */\nconst PORT =\n Number(process.env.PORT) ||\n Number(process.env.CASTLE_GATEWAY_PORT) ||\n 8090;\n/** Spiral / world WebSocket server — set after `wss` init; used by genesis runtime surface. */\nlet spiralWssForGenesis = null;\nconst MAX_MESSAGE_BYTES = Number(process.env.CASTLE_MAX_MESSAGE_BYTES || 32 * 1024);\nconst REQUIRED_GATEWAY_TOKEN = process.env.CASTLE_GATEWAY_TOKEN || \"\";\nconst ALLOWED_ORIGINS = parseAllowedOriginsFromEnv().fromList;\nconst httpCors = createHttpCorsPolicy();\n\n/** @returns {string|null} `Access-Control-Allow-Origin`; null = browser CORS fail. */\nfunction accessControlAllowOriginValue(req) {\n return httpCors.accessControlAllowOriginValue(req);\n}\n\nfunction applyHttpCorsHeaders(req, res) {\n return httpCors.applyHttpCorsHeaders(req, res);\n}\nconst REQUIRE_AUTH = process.env.CASTLE_REQUIRE_AUTH === \"true\";\nconst ALLOW_DEV_ANON = process.env.CASTLE_ALLOW_DEV_ANON !== \"false\";\nconst ALLOW_DEV_HTTP_UID = process.env.CASTLE_ALLOW_DEV_HTTP_UID !== \"false\";\n/** Presence mesh: allow `X-Castle-Guest-Id` when Firebase auth absent (default on). Set `false` to require auth only. */\nconst ALLOW_MESH_GUEST = process.env.CASTLE_ALLOW_MESH_GUEST !== \"false\";\nconst SOCIAL_RETRY_MAX = Math.max(0, Math.min(5, Number(process.env.CASTLE_SOCIAL_RETRY_MAX || 2)));\nconst SOCIAL_RETRY_BASE_MS = Math.max(100, Number(process.env.CASTLE_SOCIAL_RETRY_BASE_MS || 700));\nconst TELEGRAM_BOT_TOKEN = process.env.CASTLE_TELEGRAM_BOT_TOKEN || \"\";\nconst WHATSAPP_TOKEN = process.env.CASTLE_WHATSAPP_TOKEN || \"\";\nconst WHATSAPP_PHONE_NUMBER_ID = process.env.CASTLE_WHATSAPP_PHONE_NUMBER_ID || \"\";\nconst RL_RHIZOH_LLM_PER_MIN = Math.max(5, Number(process.env.CASTLE_RL_RHIZOH_LLM_PER_MIN || 40));\nconst RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN || 30)\n);\nconst RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN || 120));\nconst RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN = Math.max(\n 10,\n Number(process.env.CASTLE_RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN || 60)\n);\nconst RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN = Math.max(8, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN || 48));\nconst RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN = Math.max(4, Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN || 24));\nconst RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN || 18)\n);\nconst RL_EPISTEMIC_SEAL_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_EPISTEMIC_SEAL_PER_MIN || 120));\nconst RL_EPISTEMIC_LOG_PER_MIN = Math.max(20, Number(process.env.CASTLE_RL_EPISTEMIC_LOG_PER_MIN || 240));\nconst EPISTEMIC_SEAL_SECRET = String(\n process.env.CASTLE_EPISTEMIC_SEAL_SECRET || process.env.CASTLE_GATEWAY_TOKEN || \"\"\n).trim();\nconst RL_LLM_CONN_TEST_PER_MIN = Math.max(2, Number(process.env.CASTLE_RL_LLM_CONNECTION_TEST_PER_MIN || 8));\n\n/** One-shot production warnings: CORS surface, auth toggle, dev bypass flags (observatory deploy hygiene). */\nfunction logProductionObservatorySurfaceGuardsV0() {\n if (String(process.env.NODE_ENV || \"\").toLowerCase() !== \"production\") return;\n const tag = \"[GATEWAY_PROD_GUARD]\";\n const primaryCors = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n const corsListEmpty = ALLOWED_ORIGINS.length === 0;\n if (corsListEmpty && !primaryCors) {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty and CASTLE_HTTP_CORS_ORIGIN empty — production still merges RHIZOH_BROWSER_ORIGINS_V1 defaults; set env for preview/staging hosts`\n );\n } else if (corsListEmpty && primaryCors && primaryCors !== \"*\") {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty — only CASTLE_HTTP_CORS_ORIGIN set; add web.app + firebaseapp.com (and previews) to CASTLE_ALLOWED_ORIGINS if clients use alternate hostnames`\n );\n }\n if (primaryCors && /localhost|127\\.0\\.0\\.1/i.test(primaryCors)) {\n console.warn(`${tag} CORS: CASTLE_HTTP_CORS_ORIGIN is localhost while NODE_ENV=production`);\n }\n if (REQUIRE_AUTH) {\n console.warn(\n `${tag} AUTH: CASTLE_REQUIRE_AUTH=true — clients must send a valid gateway credential (otherwise silent 401 on replay/SSE); EventSource cannot set custom headers unless proxied`\n );\n if (REQUIRED_GATEWAY_TOKEN.length < 16) {\n console.warn(`${tag} AUTH: CASTLE_GATEWAY_TOKEN missing or short while require-auth enabled`);\n }\n }\n if (ALLOW_DEV_ANON) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_ANON is not \"false\" — dev anonymous path may be live in production (set CASTLE_ALLOW_DEV_ANON=false)`\n );\n }\n if (ALLOW_DEV_HTTP_UID) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_HTTP_UID is not \"false\" — X-Castle-Dev-Uid bypass risk (set CASTLE_ALLOW_DEV_HTTP_UID=false)`\n );\n }\n}\n\nconst rhizohRuntime = initRhizoh();\n\n/** İstemci: llmKeySource veya keySource — env | user_connection | auto */\nfunction normalizeClientLlmKeySource(raw) {\n const s = String(raw ?? \"\").trim().toLowerCase();\n if (s === \"env\" || s === \"server\") return \"env\";\n if (s === \"user_connection\" || s === \"connection\" || s === \"user\") return \"user_connection\";\n if (s === \"auto\" || s === \"\") return \"auto\";\n return null;\n}\n\n/**\n * Server-side provider wins for env/auto unless user_connection — client bundle may still send provider=openai.\n * @param {\"env\"|\"user_connection\"|\"auto\"} keyMode\n * @param {{ provider?: string, model?: string, apiKey?: string } | null} conn\n * @param {{ provider?: string, model?: string }} payload\n */\nfunction resolveServerLlmProviderV0(keyMode, conn, payload) {\n const envProvider = String(process.env.CASTLE_LLM_PROVIDER || \"\").trim() || undefined;\n const envModel = String(process.env.CASTLE_LLM_MODEL || \"\").trim() || undefined;\n\n if (keyMode === \"user_connection\") {\n return {\n resolvedProvider: conn?.provider || payload?.provider,\n resolvedModel: conn?.model || payload?.model,\n connApiKey: conn?.apiKey || \"\"\n };\n }\n\n if (keyMode === \"env\") {\n return {\n resolvedProvider: envProvider ?? payload?.provider,\n resolvedModel: envModel ?? payload?.model,\n connApiKey: \"\"\n };\n }\n\n if (conn?.provider && conn?.apiKey) {\n return {\n resolvedProvider: conn.provider,\n resolvedModel: conn.model,\n connApiKey: conn.apiKey\n };\n }\n\n return {\n resolvedProvider: envProvider ?? payload?.provider,\n resolvedModel: envModel ?? payload?.model,\n connApiKey: \"\"\n };\n}\n\nfunction jsonReplacerSafeV0(_key, value) {\n if (typeof value === \"bigint\") return Number(value);\n return value;\n}\n\nfunction sendJson(res, statusCode, payload) {\n res.writeHead(statusCode, { \"Content-Type\": \"application/json; charset=utf-8\" });\n try {\n res.end(JSON.stringify(payload, jsonReplacerSafeV0));\n } catch (error) {\n res.end(\n JSON.stringify({\n ok: false,\n error: \"response_serialize_failed\",\n detail: String(error?.message || error)\n })\n );\n }\n}\n\nfunction logRhizohHealth(stage, detail = {}) {\n try {\n console.info(`[CASTLE_${String(stage || \"unknown\")}]`, detail && typeof detail === \"object\" ? detail : {});\n } catch {\n /* noop */\n }\n}\n\nfunction renderInfraMetricsProm() {\n const lines = [\n `castle_gateway_events_processed_total ${infraMetrics.eventsProcessed}`,\n `castle_gateway_duplicate_reject_total ${infraMetrics.duplicateRejects}`,\n `castle_gateway_enqueue_latency_ms ${infraMetrics.enqueueLatencyMs}`,\n `castle_gateway_queue_depth ${infraMetrics.queueDepth}`,\n `castle_gateway_queue_lag_seconds ${(Number(infraMetrics.queueLag || 0) / 1000).toFixed(3)}`,\n `castle_gateway_errors_total ${infraMetrics.errors}`\n ];\n const buckets = infraMetrics.enqueueLatencyBuckets || {};\n for (const k of Object.keys(buckets)) {\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"${k}\"} ${buckets[k]}`);\n }\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"+Inf\"} ${infraMetrics.eventsProcessed}`);\n if (process.env.CASTLE_SUBSTRATE_METRICS !== \"0\") {\n lines.push(renderSubstratePrometheusMetricsV0().trimEnd());\n }\n return lines.join(\"\\n\");\n}\n\nasync function fetchJsonWithTimeout(url, timeoutMs = 1200) {\n const ctl = new AbortController();\n const tid = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const res = await fetch(url, { signal: ctl.signal });\n if (!res.ok) return null;\n return await res.json();\n } catch {\n return null;\n } finally {\n clearTimeout(tid);\n }\n}\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction getHttpPathname(req) {\n let p = String(req.url || \"\").split(\"?\")[0];\n /* Reverse proxies / clients sometimes append `/` — avoid silent 404 on e.g. `/rhizoh/genesis/stream/`. */\n while (p.length > 1 && p.endsWith(\"/\")) {\n p = p.slice(0, -1);\n }\n return p;\n}\n\nfunction rhizohLlmEnvConfigured() {\n const p = String(process.env.CASTLE_LLM_PROVIDER || \"openai\").toLowerCase();\n const key =\n p === \"anthropic\"\n ? process.env.ANTHROPIC_API_KEY\n : p === \"gemini\"\n ? process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY\n : p === \"xai\"\n ? process.env.XAI_API_KEY\n : p === \"deepseek\"\n ? process.env.DEEPSEEK_API_KEY\n : p === \"mistral\"\n ? process.env.MISTRAL_API_KEY\n : p === \"openrouter\"\n ? process.env.OPENROUTER_API_KEY\n : process.env.OPENAI_API_KEY;\n return !!String(key || \"\").trim();\n}\n\n/**\n * @param {import(\"firebase-admin/firestore\").Firestore | null} db\n * @param {number} timeoutMs\n */\nasync function probeFirestoreReachable(db, timeoutMs = 2000) {\n if (!db) return { ok: false, reason: \"no_db\" };\n const timeout = sleep(timeoutMs).then(() => ({ ok: false, reason: \"timeout\" }));\n const work = db\n .collection(\"_castle_gateway_health\")\n .doc(\"ping\")\n .get()\n .then(() => ({ ok: true, reason: \"reachable\" }))\n .catch((e) => ({ ok: false, reason: String(e?.message || e || \"firestore_error\") }));\n return Promise.race([work, timeout]);\n}\n\nfunction stripHtml(text = \"\") {\n return String(text)\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \" \")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \" \")\n .replace(/<[^>]+>/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction haversineMeters(aLat, aLon, bLat, bLon) {\n const R = 6371000;\n const dLat = ((bLat - aLat) * Math.PI) / 180;\n const dLon = ((bLon - aLon) * Math.PI) / 180;\n const sa = Math.sin(dLat / 2);\n const sb = Math.sin(dLon / 2);\n const x = sa * sa + Math.cos((aLat * Math.PI) / 180) * Math.cos((bLat * Math.PI) / 180) * sb * sb;\n return 2 * R * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));\n}\n\nasync function checkPublishTargetHealth(protocol, target) {\n const p = String(protocol || \"\").toUpperCase();\n const t = String(target || \"\").trim();\n if (!t) return { ok: false, reason: \"target_missing\", shouldReconnect: false };\n if (p === \"RTMP\") {\n const ok = t.startsWith(\"rtmp://\") || t.startsWith(\"rtmps://\");\n return { ok, reason: ok ? \"rtmp_target_format_ok\" : \"invalid_rtmp_target\", shouldReconnect: !ok, reconnectInSec: 15 };\n }\n if (p === \"WHIP\" || p === \"SFU\") {\n try {\n const res = await fetch(t, { method: \"OPTIONS\" });\n const ok = res.status < 500;\n return { ok, reason: `http_${res.status}`, shouldReconnect: !ok, reconnectInSec: 8 };\n } catch {\n return { ok: false, reason: \"network_unreachable\", shouldReconnect: true, reconnectInSec: 8 };\n }\n }\n return { ok: true, reason: \"unchecked_protocol\", shouldReconnect: false };\n}\n\nasync function fetchTextFromUrl(url) {\n const res = await fetch(url, { method: \"GET\" });\n if (!res.ok) throw new Error(`fetch_${res.status}`);\n const ct = String(res.headers.get(\"content-type\") || \"\");\n if (ct.includes(\"application/pdf\")) throw new Error(\"pdf_binary_not_supported_directly\");\n const body = await res.text();\n return stripHtml(body).slice(0, 14000);\n}\n\nfunction buildTaskActionGraph(task = \"\", context = {}) {\n const t = String(task || \"\").toLowerCase();\n if (t.includes(\"dinner\") || t.includes(\"akşam yemeği\") || t.includes(\"aksam yemegi\")) {\n return {\n taskTitle: \"Dinner Prep Planner\",\n nodes: [\n { id: \"n1\", title: \"Kitchen preheat\", action: \"smart-oven.preheat\", params: { temperatureC: 190, mode: \"fan\" } },\n { id: \"n2\", title: \"Lighting comfort\", action: \"smart-light.set_scene\", params: { scene: \"warm_dinner\" } },\n { id: \"n3\", title: \"Fridge inventory check\", action: \"smart-fridge.inventory_scan\", params: {} },\n { id: \"n4\", title: \"Reminder notification\", action: \"smart-watch.notify\", params: { text: \"Dinner prep started.\" } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" },\n { from: \"n3\", to: \"n4\" }\n ]\n };\n }\n return {\n taskTitle: task || \"General Planner\",\n nodes: [\n { id: \"n1\", title: \"Safety check\", action: \"device.safety_check\", params: { context } },\n { id: \"n2\", title: \"Primary action\", action: \"device.execute_task\", params: { task } },\n { id: \"n3\", title: \"Report\", action: \"studio.timeline_note\", params: { task } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" }\n ]\n };\n}\n\nfunction buildEthicsPrograms() {\n return {\n policyVersion: \"v1\",\n tracks: [\n {\n id: \"elderly-care-assist\",\n audience: \"elderly\",\n title: \"Elderly Assistance Track\",\n principles: [\"human-in-the-loop\", \"consent-first\", \"low-friction alerts\"],\n modules: [\"medication-reminder\", \"fall-risk-check\", \"family-notify\", \"daily-companion-brief\"]\n },\n {\n id: \"kids-safe-learning\",\n audience: \"children\",\n title: \"Kids Safe Learning Track\",\n principles: [\"guardian-consent\", \"age-appropriate-language\", \"content-filtering\"],\n modules: [\"study-coach\", \"screen-time-balance\", \"safe-social-share\", \"emergency-contact\"]\n }\n ]\n };\n}\n\nasync function deliverViaTelegram(channel, text) {\n const chatId = String(channel.endpoint || \"\").trim();\n if (!TELEGRAM_BOT_TOKEN) throw new Error(\"telegram_token_missing\");\n if (!chatId) throw new Error(\"telegram_chat_id_missing\");\n const url = `https://api.telegram.org/bot${encodeURIComponent(TELEGRAM_BOT_TOKEN)}/sendMessage`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ chat_id: chatId, text })\n });\n if (!res.ok) throw new Error(`telegram_http_${res.status}`);\n return { provider: \"telegram\", status: res.status };\n}\n\nasync function deliverViaWhatsapp(channel, text) {\n const to = String(channel.endpoint || \"\").trim();\n if (!WHATSAPP_TOKEN) throw new Error(\"whatsapp_token_missing\");\n if (!WHATSAPP_PHONE_NUMBER_ID) throw new Error(\"whatsapp_phone_number_id_missing\");\n if (!to) throw new Error(\"whatsapp_to_missing\");\n const url = `https://graph.facebook.com/v22.0/${encodeURIComponent(WHATSAPP_PHONE_NUMBER_ID)}/messages`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${WHATSAPP_TOKEN}` },\n body: JSON.stringify({ messaging_product: \"whatsapp\", to, type: \"text\", text: { body: text } })\n });\n if (!res.ok) throw new Error(`whatsapp_http_${res.status}`);\n return { provider: \"whatsapp\", status: res.status };\n}\n\nasync function deliverViaWebhook(channel, text) {\n const endpoint = String(channel.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"webhook_endpoint_missing\");\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text, platform: channel.platform || \"webhook\", ts: Date.now() })\n });\n if (!res.ok) throw new Error(`webhook_http_${res.status}`);\n return { provider: \"webhook\", status: res.status };\n}\n\nasync function deliverSocialMessage(channel, text) {\n const platform = String(channel.platform || \"webhook\").toLowerCase();\n if (platform === \"telegram\") return deliverViaTelegram(channel, text);\n if (platform === \"whatsapp\") return deliverViaWhatsapp(channel, text);\n return deliverViaWebhook(channel, text);\n}\n\nasync function deliverSocialWithRetry(channel, text) {\n let attempt = 0;\n let lastError = null;\n while (attempt <= SOCIAL_RETRY_MAX) {\n try {\n const result = await deliverSocialMessage(channel, text);\n return { ok: true, attempt: attempt + 1, ...result };\n } catch (error) {\n lastError = error;\n if (attempt >= SOCIAL_RETRY_MAX) break;\n const jitter = Math.floor(Math.random() * 90);\n await sleep(SOCIAL_RETRY_BASE_MS * Math.pow(2, attempt) + jitter);\n }\n attempt += 1;\n }\n return { ok: false, attempt: attempt + 1, error: String(lastError?.message || \"delivery_failed\") };\n}\n\nasync function dispatchCommandToAdapter(device, command) {\n const adapter = String(command?.adapter || device?.adapter || \"websocket\").toLowerCase();\n const endpoint = String(device?.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"device_endpoint_missing\");\n if (adapter === \"mqtt\") {\n // MQTT delivery is proxied via a bridge endpoint (HTTP webhook style).\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n type: \"mqtt-command\",\n topic: `castle/devices/${device.id}/commands`,\n action: command.action,\n params: command.params || {}\n })\n });\n if (!res.ok) throw new Error(`mqtt_bridge_http_${res.status}`);\n return { ok: true, adapter, via: \"http-bridge\", status: res.status };\n }\n if (!endpoint.startsWith(\"ws://\") && !endpoint.startsWith(\"wss://\")) throw new Error(\"invalid_websocket_endpoint\");\n const sendResult = await new Promise((resolve, reject) => {\n const ws = new WebSocket(endpoint);\n const timer = setTimeout(() => {\n try {\n ws.close();\n } catch {\n /* noop */\n }\n reject(new Error(\"websocket_timeout\"));\n }, 3000);\n ws.on(\"open\", () => {\n ws.send(\n JSON.stringify({\n type: \"castle-command\",\n commandId: command.id,\n deviceId: command.deviceId,\n action: command.action,\n params: command.params || {}\n }),\n (err) => {\n clearTimeout(timer);\n ws.close();\n if (err) reject(new Error(\"websocket_send_failed\"));\n else resolve({ ok: true, adapter: \"websocket\", via: \"ws\" });\n }\n );\n });\n ws.on(\"error\", () => {\n clearTimeout(timer);\n reject(new Error(\"websocket_connect_failed\"));\n });\n });\n return sendResult;\n}\n\nfunction readHttpJson(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => {\n if (!buf) return resolve({});\n const payload = safeJsonParse(buf);\n if (!payload || typeof payload !== \"object\") return reject(new Error(\"invalid_json_payload\"));\n resolve(payload);\n });\n req.on(\"error\", reject);\n });\n}\n\n/** Ham gövde (HMAC doğrulaması için). */\nfunction readHttpBodyText(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => resolve(buf));\n req.on(\"error\", reject);\n });\n}\n\nfunction maskOpaqueId(value) {\n const t = String(value || \"\");\n if (!t) return \"(empty)\";\n if (t.length <= 8) return \"****\";\n return `${t.slice(0, 4)}…${t.slice(-2)}`;\n}\n\nfunction readGatewayHttpToken(req) {\n const x = String(req.headers?.[\"x-castle-gateway-token\"] || \"\").trim();\n if (x) return x;\n const auth = String(req.headers?.authorization || \"\");\n const m = /^Bearer\\s+(.+)$/i.exec(auth);\n return m ? String(m[1] || \"\").trim() : \"\";\n}\n\nasync function resolveHttpUser(req) {\n const authResult = await verifyClientToken(req);\n if (authResult?.ok && authResult.user?.uid) {\n return { ok: true, uid: String(authResult.user.uid), mode: authResult.kind || \"auth\" };\n }\n if (ALLOW_DEV_ANON && ALLOW_DEV_HTTP_UID) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) return { ok: true, uid: `dev-${devUid.slice(0, 64)}`, mode: \"dev-header\" };\n }\n return { ok: false, reason: authResult?.reason || \"auth_required\" };\n}\n\n/**\n * Epistemic ledger ingest — Firebase uid OR valid transport token + X-Castle-Dev-Uid.\n * Prod disables CASTLE_ALLOW_DEV_HTTP_UID; LLM may still work while telemetry 401'd without this.\n */\nasync function resolveEpistemicIngestActor(req) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok && tok === REQUIRED_GATEWAY_TOKEN) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) {\n return { ok: true, uid: `gw-telemetry-${devUid.slice(0, 64)}`, mode: \"gateway-token-dev-uid\" };\n }\n return { ok: false, reason: \"dev_uid_required\" };\n }\n if (tok) {\n return { ok: false, reason: \"gateway_token_mismatch\" };\n }\n }\n const auth = await resolveHttpUser(req);\n if (auth.ok) return auth;\n return { ok: false, reason: auth.reason || \"auth_required\" };\n}\n\n/** Mesh actor: Firebase uid, dev header, or `guest:<uuid>` via header / `guestId` query (SSE cannot set headers). */\nasync function resolveMeshActor(req) {\n const auth = await resolveHttpUser(req);\n if (auth.ok) return { ok: true, clientUid: auth.uid, mode: auth.mode };\n if (ALLOW_MESH_GUEST) {\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const gid = String(req.headers?.[\"x-castle-guest-id\"] || q.searchParams.get(\"guestId\") || \"\").trim();\n if (gid) {\n const normalized = gid.startsWith(\"guest:\") ? gid.slice(0, 180) : `guest:${gid.slice(0, 160)}`;\n return { ok: true, clientUid: normalized, mode: \"guest-header\" };\n }\n }\n return { ok: false, reason: \"mesh_auth_required\" };\n}\n\n/** Live genesis runtime JSON (GET /rhizoh/genesis/runtime + SSE infra sampler). */\nasync function buildGenesisRuntimeSurfacePayloadLive() {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n let spiralWsActive = 0;\n if (spiralWssForGenesis) {\n for (const c of spiralWssForGenesis.clients) {\n if (c.readyState === 1) spiralWsActive += 1;\n }\n }\n const base = buildGenesisRuntimeSurfacePayload({\n infraMetrics,\n scoredHealth: scored,\n rhizohEnterpriseMetrics,\n mesh: meshContinuityAggregate(),\n workerHealth: worker,\n port: PORT,\n spiralWebSocketClientsActive: spiralWsActive\n });\n return {\n ...base,\n replayFingerprint: computeGenesisReplayFingerprintV0(base),\n gatewayCapabilities: buildGenesisGatewayCapabilitiesV0(),\n genesisStream: {\n schema: \"castle.genesis.stream_cursor.v0\",\n lastAcceptedSeq: getGenesisContinuitySeq(),\n clientIngress: {\n schema: GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n contract: \"castle.world_observation.ingress_envelope.v1\",\n postPath: rhizohRuntime.routes.genesisIngress,\n closure: getGenesisWorldObservationIngressStatusV0()\n },\n seqContinuity: getGenesisContinuitySeqAuditV0(),\n eventArchive: genesisContinuityEventArchiveEnabled()\n ? {\n enabled: true,\n projection: GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION,\n queryPath: rhizohRuntime.routes.genesisContinuityEvents\n }\n : { enabled: false },\n replayRouter: {\n schema: GENESIS_REPLAY_ROUTER_SCHEMA,\n queryPath: rhizohRuntime.routes.genesisReplay,\n queryHint: \"from=&to= or range=from-to; optional type=&checkpoints=0\",\n temporalDiffPath: rhizohRuntime.routes.genesisReplayDiff,\n temporalDiffSchema: GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA,\n equivalencePath: rhizohRuntime.routes.genesisReplayEquivalence,\n analyticsPath: rhizohRuntime.routes.genesisReplayAnalytics,\n evolutionPath: rhizohRuntime.routes.genesisReplayEvolution\n }\n }\n };\n}\n\nconst httpServer = createServer(async (req, res) => {\n const corsAllow = applyHttpCorsHeaders(req, res);\n if (req.method === \"OPTIONS\") {\n if (!corsAllow && req.headers?.origin) {\n console.warn(\"[GATEWAY_CORS] preflight rejected for origin:\", req.headers.origin);\n }\n res.writeHead(corsAllow ? 204 : 403);\n res.end();\n return;\n }\n\n const pathname = getHttpPathname(req);\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/genesis/__ping\") {\n sendJson(res, 200, {\n ok: true,\n pid: process.pid,\n entry: __filename,\n time: Date.now(),\n genesis: true\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/epistemic/__probe\") {\n sendJson(res, 200, {\n ok: true,\n epistemicIngestActorV2: true,\n routes: {\n seal: rhizohRuntime.routes.epistemicSeal,\n logsBatch: rhizohRuntime.routes.epistemicLogsBatch\n },\n hasGatewayToken: Boolean(REQUIRED_GATEWAY_TOKEN),\n gatewayTokenLen: REQUIRED_GATEWAY_TOKEN ? REQUIRED_GATEWAY_TOKEN.length : 0\n });\n return;\n }\n\n if (\n req.method === \"GET\" &&\n (pathname === \"/rhizoh/live/world-feed\" ||\n pathname === \"/rhizoh/live/sports-bundle\" ||\n pathname === \"/rhizoh/live/news-headlines\")\n ) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_live_world_feed:${ip}`, RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const out =\n pathname === \"/rhizoh/live/sports-bundle\"\n ? await handleLiveSportsBundleGetV0()\n : pathname === \"/rhizoh/live/news-headlines\"\n ? await handleLiveNewsHeadlinesGetV0(req)\n : await handleLiveWorldFeedGetV0(req);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/webhooks/stripe\") {\n await handleStripeMembershipWebhook(req, res);\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/billing/stripe/checkout\") {\n await handleStripeCheckoutCreate(req, res, { readHttpJson, resolveHttpUser, sendJson });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/live\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, live: false, dns: true, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n sendJson(res, 200, { ok: true, live: true, dns: true, service: \"castle-gateway\", ts: Date.now() });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/cors\") {\n const reqOrigin = normalizeHttpOrigin(req.headers?.origin);\n const allowOrigin = accessControlAllowOriginValue(req);\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n requestOrigin: reqOrigin || null,\n allowed: Boolean(allowOrigin),\n allowOrigin: allowOrigin || null,\n allowSetSize: httpCors.allowSet.size\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/ready\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, ready: false, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n try {\n const persistence = getFirebasePersistence();\n const llmProbe = rhizohLlmEnvConfigured();\n sendJson(res, 200, {\n ok: true,\n ready: true,\n dns: true,\n persistence: persistence.mode,\n service: \"castle-gateway\",\n gateway: \"ok\",\n llm: llmProbe ? \"ok\" : \"degraded\",\n spine: llmProbe ? \"ok\" : \"partial\"\n });\n } catch (e) {\n sendJson(res, 503, { ok: false, ready: false, reason: String(e?.message || e), service: \"castle-gateway\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/reality\") {\n const payload = buildRealityHealthPayload();\n sendJson(res, payload.ok ? 200 : 503, payload);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/substrate\") {\n const authority = buildGatewaySubstrateAuthoritySnapshotV0();\n const operational = buildSubstrateOperationalSnapshotV0();\n const drift = buildRealityHealthPayload();\n const ok = Boolean(authority.productionAuthoritySatisfied) && drift.ok;\n sendJson(res, ok ? 200 : 503, {\n ok,\n service: \"castle-gateway\",\n ts: Date.now(),\n authority,\n operational,\n drift: { ok: drift.ok, errors: drift.drift?.errors, warnings: drift.drift?.warnings }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/substrate/health\") {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const body = await readHttpJson(req);\n const uid = (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestClientSubstrateHealthV0(body, { uid, clientId: uid });\n if (!result.ok) {\n sendJson(res, 400, result);\n return;\n }\n recordSubstrateOtelEventV0(\"client_health_ingest\", { uid: String(uid).slice(0, 32) });\n sendJson(res, 200, result);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/deps\") {\n const t0 = Date.now();\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, {\n ok: false,\n dns: true,\n llm: false,\n firestore: false,\n phase: \"maintenance\",\n latencyMs: Date.now() - t0,\n service: \"castle-gateway\"\n });\n return;\n }\n const persistence = getFirebasePersistence();\n let firestoreOk = persistence.mode !== \"firebase\";\n let firestoreDetail = persistence.mode === \"file\" ? \"file_mode\" : \"not_applicable\";\n if (persistence.mode === \"firebase\" && persistence.db) {\n const pr = await probeFirestoreReachable(persistence.db, 2000);\n firestoreOk = pr.ok;\n firestoreDetail = pr.reason || (pr.ok ? \"reachable\" : \"fail\");\n } else if (persistence.mode === \"firebase\" && !persistence.db) {\n firestoreOk = false;\n firestoreDetail = \"firebase_not_initialized\";\n }\n const llmOk = rhizohLlmEnvConfigured();\n const overallOk = llmOk && firestoreOk;\n const latencyMs = Date.now() - t0;\n sendJson(res, 200, {\n ok: overallOk,\n dns: true,\n llm: llmOk,\n firestore: persistence.mode === \"firebase\" ? firestoreOk : false,\n persistence: persistence.mode,\n firestoreDetail,\n latencyMs,\n service: \"castle-gateway\",\n wsPort: PORT\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health\") {\n const persistence = getFirebasePersistence();\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n wsPort: PORT,\n persistence: persistence.mode,\n presenceMesh: \"sse\"\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/health\") {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n sendJson(res, 200, {\n ok: true,\n role: \"gateway\",\n queueLagMs: infraMetrics.queueLag,\n queueDepth: infraMetrics.queueDepth,\n errors: infraMetrics.errors,\n status: scored.status,\n score: scored.score,\n reasons: scored.reasons,\n worker: worker\n ? {\n status: worker.status,\n score: worker.score,\n reasons: worker.reasons\n }\n : null,\n timestamp: Date.now()\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisRuntime) {\n const payload = await buildGenesisRuntimeSurfacePayloadLive();\n sendJson(res, 200, payload);\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.genesisIngress) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const actor = await resolveMeshActor(req);\n const clientId = actor.ok ? actor.clientUid : (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestGenesisWorldObservationV0(body, { clientId });\n if (!result.ok) {\n const status =\n result.error === \"rate_limited\" || result.error === \"agent_spoke_throttled\"\n ? 429\n : result.error === \"payload_too_large\"\n ? 413\n : 400;\n sendJson(res, status, result);\n return;\n }\n sendJson(res, result.idempotent ? 200 : 202, result);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisStream) {\n const corsOrigin = accessControlAllowOriginValue(req);\n if (!corsOrigin && String(process.env.NODE_ENV || \"\").toLowerCase() === \"production\") {\n const og = String(req.headers?.origin || \"\").trim();\n if (og) {\n console.warn(\"[GATEWAY] genesis SSE: missing Access-Control-Allow-Origin for browser Origin (CORS whitelist mismatch):\", og);\n }\n }\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n res.write(\"retry: 3000\\n\\n\");\n res.write(\": genesis continuity stream\\n\\n\");\n res.write(\"event: genesis\\n\");\n res.write(`data: {\"booted\":true,\"pid\":${process.pid}}\\n\\n`);\n registerGenesisSseClient(res, req);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLatest) {\n const cp = getLatestGenesisSignedCheckpoint();\n if (!cp) {\n sendJson(res, 404, { ok: false, error: \"no_checkpoint_yet\", hint: \"wait_for_seq_interval_or_configure_signing_secret\" });\n return;\n }\n sendJson(res, 200, { ok: true, checkpoint: cp });\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(rhizohRuntime.routes.genesisCheckpointBySeqPrefix)) {\n const tail = pathname.slice(rhizohRuntime.routes.genesisCheckpointBySeqPrefix.length).split(\"/\")[0];\n const seq = parseInt(String(tail || \"\").trim(), 10);\n const out = await genesisCheckpointQueryBySeqV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"checkpoint_not_found\") {\n sendJson(res, 404, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointRange) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryRangeV0(from, to);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"range_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLineage) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const seq = parseInt(String(u.searchParams.get(\"seq\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryLineageV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"lineage_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisContinuityEvents) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const limit = parseInt(String(u.searchParams.get(\"limit\") || \"\").trim(), 10);\n const out = await queryGenesisContinuityEventArchiveV0(from, to, type, limit);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\" || out.error === \"event_archive_disabled\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"archive_file_too_large\") {\n sendJson(res, 413, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplay) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await resolveGenesisReplayRouterV1({\n from: fromSeq,\n to: toSeq,\n type,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayDiff) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const out = await computeGenesisReplayTemporalDiffV1({ from: fromSeq, to: toSeq, type });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEquivalence) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from1 = parseInt(String(u.searchParams.get(\"from1\") || \"\").trim(), 10);\n const to1 = parseInt(String(u.searchParams.get(\"to1\") || \"\").trim(), 10);\n const from2 = parseInt(String(u.searchParams.get(\"from2\") || \"\").trim(), 10);\n const to2 = parseInt(String(u.searchParams.get(\"to2\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await compareGenesisReplayEquivalenceV1({\n from1,\n to1,\n from2,\n to2,\n type,\n includeCheckpoints\n });\n if (\n !out.ok &&\n (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")\n ) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayAnalytics) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayAnalyticsV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEvolution) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const collapseWindows = parseInt(String(u.searchParams.get(\"collapseWindows\") || \"0\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayEvolutionV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n collapseWindows,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/metrics\") {\n res.writeHead(200, { \"Content-Type\": \"text/plain; version=0.0.4; charset=utf-8\" });\n res.end(renderInfraMetricsProm());\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.infraPrometheusMetrics) {\n if (process.env.CASTLE_PROMETHEUS_METRICS !== \"1\") {\n sendJson(res, 404, { ok: false, error: \"prometheus_metrics_disabled\" });\n return;\n }\n res.writeHead(200, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(renderRhizohPrometheusMetrics());\n return;\n }\n if (req.method === \"GET\" && pathname.startsWith(\"/infra/traces/\")) {\n const traceId = decodeURIComponent(pathname.slice(\"/infra/traces/\".length));\n const item = await getTraceById(traceId);\n if (!item) return sendJson(res, 404, { ok: false, error: \"trace_not_found\" });\n return sendJson(res, 200, { ok: true, trace: item });\n }\n if (req.method === \"GET\" && pathname === \"/infra/traces\") {\n const u = new URL(String(req.url || \"/infra/traces\"), `http://${req.headers.host || \"localhost\"}`);\n const sessionId = String(u.searchParams.get(\"sessionId\") || \"\").trim();\n const items = sessionId ? await listTracesBySession(sessionId) : [];\n return sendJson(res, 200, { ok: true, sessionId, traces: items });\n }\n\n /** Presence room mesh (C): JOIN / LEAVE / SNAPSHOT / DELTA / REPLAY (HTTP); SUBSCRIBE = SSE stream. */\n if (req.method === \"POST\" && pathname === \"/presence/mesh/join\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid, clientUid: actor.clientUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/leave\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshLeave(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/snapshot\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n sendJson(res, 200, meshSnapshot(roomUid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/delta\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, MAX_MESSAGE_BYTES);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const seq = meshAppendDelta(roomUid, actor.clientUid, {\n node: body?.node,\n projectionPatch: body?.projectionPatch,\n writerSubject: body?.writerSubject\n });\n sendJson(res, 200, { ok: true, roomUid, seq });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/replay\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const fromNodeId = body?.fromNodeId != null ? String(body.fromNodeId) : \"\";\n const fromSeq = body?.fromSeq;\n if (fromNodeId) {\n sendJson(res, 200, meshReplayFromNodeId(roomUid, fromNodeId));\n return;\n }\n sendJson(res, 200, meshReplayFromSeq(roomUid, fromSeq));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/presence/mesh/subscribe\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const roomUid = String(q.searchParams.get(\"roomUid\") || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n const write = (chunk) => {\n try {\n res.write(chunk);\n } catch {\n /* closed */\n }\n };\n const unsub = meshSubscribeSse(roomUid, write);\n const onClose = () => {\n unsub();\n meshLeave(roomUid, actor.clientUid);\n try {\n res.end();\n } catch {\n /* ignore */\n }\n };\n req.on(\"close\", onClose);\n req.on(\"aborted\", onClose);\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, uid: auth.uid, mode: auth.mode, items: await listConnections(auth.uid) });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const id = await createConnection(auth.uid, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"create_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n await updateConnection(auth.uid, id, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"update_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"DELETE\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n await deleteConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"delete_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/rhizoh/sync/vault\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const snapshot = await getCloudSyncSnapshotV0(auth.uid);\n sendJson(res, 200, { ok: true, uid: auth.uid, snapshot });\n } catch (error) {\n sendJson(res, 500, { ok: false, error: error?.message || \"cloud_sync_pull_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/rhizoh/sync/vault\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const snapshot = await mergeCloudSyncSnapshotV0(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, uid: auth.uid, snapshot });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"cloud_sync_push_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections/test\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n if (!checkHttpRateLimit(`llm_conn_test:${auth.uid}`, RL_LLM_CONN_TEST_PER_MIN, 60_000)) {\n return sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n }\n const hasBodyKey = !!String(payload?.apiKey || \"\").trim();\n const result = await queryRhizohLlm(\n {\n message: String(payload?.message || \"Ping from Castle Gateway\"),\n context: payload?.context || { source: \"connection_test\" },\n provider: payload?.provider,\n model: payload?.model,\n apiKey: payload?.apiKey || \"\"\n },\n { allowExternalApiKey: true, llmKeySource: hasBodyKey ? \"auto\" : \"env\" }\n );\n logLlmAccess({\n route: \"/llm/connections/test\",\n uid: auth.uid,\n llmKeyBillingOwner: result.llmKeyBillingOwner,\n llmKeyOrigin: result.llmKeyOrigin,\n provider: result.provider,\n model: result.model,\n connectionId: null\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"connection_test_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAgentIdentities(auth.uid);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerAgentIdentity({\n ownerUid: auth.uid,\n agentId: payload?.agentId,\n role: payload?.role,\n personaSeed: payload?.personaSeed || {},\n capabilityLevel: payload?.capabilityLevel\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/agents/identities/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const owns = await getAgentIdentity(id);\n if (!owns || owns.ownerUid !== auth.uid) return sendJson(res, 403, { ok: false, error: \"forbidden\" });\n const payload = await readHttpJson(req);\n const row = await updateAgentIdentity(id, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/academy/events\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/academy/events\"}`);\n const status = String(url.searchParams.get(\"status\") || \"\");\n const items = await listAcademyEvents({ ownerUid: auth.uid, status, limit: 120 });\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/academy/events\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const ev = await queueAcademyEvent({\n type: payload?.type,\n roomId: payload?.roomId,\n ownerUid: auth.uid,\n participants: payload?.participants || [],\n topic: payload?.topic || \"academy\",\n payload: payload?.payload || {}\n });\n sendJson(res, 200, { ok: true, event: ev });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && String(req.url || \"\").startsWith(\"/academy/events/\") && String(req.url || \"\").endsWith(\"/resolve\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const parts = String(req.url || \"\").split(\"/\").filter(Boolean);\n const eventId = parts[2] || \"\";\n try {\n const payload = await readHttpJson(req);\n const out = await resolveAcademyEvent(eventId, payload || {});\n sendJson(res, 200, { ok: true, event: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_resolve_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/memory\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/memory\"}`);\n const scope = url.searchParams.get(\"scope\") === \"agents\" ? \"agents\" : \"users\";\n const id = String(url.searchParams.get(\"id\") || (scope === \"agents\" ? \"\" : auth.uid));\n const kind = String(url.searchParams.get(\"kind\") || \"\");\n const limit = Number(url.searchParams.get(\"limit\") || 40);\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n sendJson(res, 200, { ok: true, scope, id, kind, items: await listMemories({ scope, id, kind, limit }) });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const profile = await getPersonaGoalMemory(auth.uid);\n sendJson(res, 200, { ok: true, profile });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const profile = await setPersonaGoalMemory({\n uid: auth.uid,\n personaPatch: payload?.personaPatch || {},\n goals: Array.isArray(payload?.goals) ? payload.goals : null,\n preferencesPatch: payload?.preferencesPatch || {}\n });\n sendJson(res, 200, { ok: true, profile });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"profile_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/compact\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const out = await autoCompactMemories({ scope, id });\n sendJson(res, 200, { ok: true, scope, id, ...out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_compact_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/context\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const context = await getMemoryContext({\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n query: String(payload?.query || \"\"),\n limit: Number(payload?.limit || 80)\n });\n sendJson(res, 200, { ok: true, context });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_context_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/place-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const placeName = String(payload?.placeName || \"\").trim();\n if (!placeName) return sendJson(res, 400, { ok: false, error: \"placeName_required\" });\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const persona = String(payload?.waypointPersona || payload?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(placeName)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Place: ${placeName}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Give a concise conversational briefing about this place for a walking-route companion.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-place-brief\", placeName, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `PLACE_BRIEF:${placeName} => ${String(summary.reply || \"\").slice(0, 900)}`,\n tags: [\"place\", \"brief\", \"event-layer\"],\n importance: 0.62,\n meta: { placeName }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"place-brief\",\n text: `${placeName} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: {\n placeName,\n directive: summary.directive,\n lat: hit?.lat ? Number(hit.lat) : null,\n lon: hit?.lon ? Number(hit.lon) : null\n }\n });\n sendJson(res, 200, { ok: true, place: hit || null, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"place_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/route-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const waypoints = Array.isArray(payload?.waypoints) ? payload.waypoints.slice(0, 24) : [];\n const out = [];\n for (const wp of waypoints) {\n const name = String(wp?.name || \"\").trim();\n if (!name) continue;\n const persona = String(wp?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(name)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Waypoint: ${name}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Generate a concise spoken briefing for route waypoint.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-route-brief\", waypoint: name, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n out.push({\n name,\n persona,\n place: hit || null,\n reply: summary.reply,\n directive: summary.directive\n });\n }\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `ROUTE_BRIEF: waypoints=${out.length} ${out.map((x) => `${x.name}:${x.persona}`).join(\" | \")}`.slice(0, 1300),\n tags: [\"route\", \"waypoint\", \"brief\", \"event-layer\"],\n importance: 0.74,\n meta: { count: out.length }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"route-brief\",\n text: out.map((x) => `${x.name}:${String(x.reply || \"\").slice(0, 140)}`).join(\" | \").slice(0, 1400),\n roomId: \"studio-main\",\n meta: {\n waypointCount: out.length,\n waypoints: out.map((x) => ({\n name: x.name,\n persona: x.persona,\n lat: x.place?.lat ? Number(x.place.lat) : null,\n lon: x.place?.lon ? Number(x.place.lon) : null\n }))\n }\n });\n sendJson(res, 200, { ok: true, items: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"route_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/pdf-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const title = String(payload?.title || \"document\");\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const providedText = String(payload?.text || \"\");\n const url = String(payload?.url || \"\");\n let content = providedText;\n if (!content && url) content = await fetchTextFromUrl(url);\n if (!content) return sendJson(res, 400, { ok: false, error: \"text_or_supported_url_required\" });\n const summary = await queryRhizohLlm(\n {\n message: `Summarize this document and provide key points + practical actions.\\nTitle:${title}\\nContent:\\n${content.slice(0, 12000)}`,\n context: { source: \"event-layer-pdf-brief\", title },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `DOC_BRIEF:${title} => ${String(summary.reply || \"\").slice(0, 1000)}`,\n tags: [\"document\", \"brief\", \"library\", \"event-layer\"],\n importance: 0.7,\n meta: { title, url: url || null }\n });\n await addTranscript(auth.uid, {\n source: \"library-brief\",\n eventType: \"pdf-brief\",\n text: `${title} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { title }\n });\n sendJson(res, 200, { ok: true, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/pdf/upload\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req, 8 * 1024 * 1024);\n const job = await enqueuePdfJob({\n uid: auth.uid,\n fileName: payload?.fileName || \"document.pdf\",\n bytesBase64: payload?.contentBase64\n });\n sendJson(res, 200, { ok: true, job });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_upload_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/session\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createPublishSession(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session\",\n text: `Publish session armed: ${row.protocol} ${row.target}`.slice(0, 500),\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: row.id, bridge: row.bridge }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/studio/publish/sessions\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listPublishSessions(auth.uid, 80);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/health-check\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const protocol = String(payload?.protocol || \"WHIP\");\n const target = String(payload?.target || \"\");\n const health = await checkPublishTargetHealth(protocol, target);\n if (payload?.sessionId) {\n await updatePublishSession(auth.uid, String(payload.sessionId), {\n status: health.ok ? \"healthy\" : \"degraded\",\n reconnectPolicy: {\n enabled: true,\n backoffSec: [2, 5, 10, 20],\n nextRetryInSec: health.reconnectInSec || 0,\n maxRetries: 8,\n reason: health.reason\n }\n });\n }\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-health\",\n text: `${protocol} ${target} => ${health.ok ? \"healthy\" : \"degraded\"} (${health.reason})`,\n roomId: \"studio-main\",\n meta: { protocol, target, reconnect: !!health.shouldReconnect }\n });\n sendJson(res, 200, { ok: true, health });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_health_check_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/studio/publish/sessions/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updatePublishSession(auth.uid, id, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session-update\",\n text: `Session ${id} -> ${row.status || \"updated\"}`,\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: id, status: row.status, ingestUrl: row.ingestUrl || \"\" }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/transcripts\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/transcripts\"}`);\n const limit = Math.max(1, Math.min(200, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await listTranscripts(auth.uid, limit);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/transcripts\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await addTranscript(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"transcript_write_failed\" });\n }\n return;\n }\n\n /** E1: broadcast intent → GreenRoom capability → transcript + memory artifact */\n if (req.method === \"POST\" && req.url === \"/studio/capabilities/greenroom\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const traceId = String(payload?.traceId || `TRC-${Date.now().toString(36).toUpperCase()}`);\n const title = String(payload?.title || \"Castle Live Broadcast\").slice(0, 200);\n const intentRaw = String(payload?.intentRaw || \"\").slice(0, 800);\n const audienceEstimate = Math.max(0, Math.min(50_000_000, Number(payload?.audienceEstimate) || 1200));\n const roomId = String(payload?.roomId || \"greenroom-main\");\n const transcriptRow = await addTranscript(auth.uid, {\n source: \"rhizoh\",\n eventType: \"BROADCAST_ROUTED\",\n text: `GreenRoom · ${title}${intentRaw ? `\\nIntent: ${intentRaw}` : \"\"}`,\n roomId,\n meta: {\n ack: \"BROADCAST_ROUTED\",\n capability: \"greenroom\",\n intentType: \"broadcast\",\n traceId,\n audienceEstimate,\n artifact: {\n kind: \"broadcast_card\",\n title,\n traceId,\n roomId,\n createdAt: Date.now()\n }\n }\n });\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n text: `GREENROOM_BROADCAST:${title} trace=${traceId}`,\n tags: [\"broadcast\", \"greenroom\", \"artifact\", \"castle-library\"],\n importance: 0.72,\n kind: \"episodic\",\n meta: { traceId, capability: \"greenroom\", transcriptId: transcriptRow.id }\n });\n sendJson(res, 200, {\n ok: true,\n ack: \"BROADCAST_ROUTED\",\n traceId,\n transcript: transcriptRow,\n replayPath: `/replay/${encodeURIComponent(traceId)}`,\n sharePath: `/replay/${encodeURIComponent(traceId)}`\n });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"greenroom_capability_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/metadata/search\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/metadata/search\"}`);\n const q = String(url.searchParams.get(\"q\") || \"\");\n const limit = Math.max(1, Math.min(300, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await searchMetadata(auth.uid, q, limit);\n sendJson(res, 200, { ok: true, q, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerDevice(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, device: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"device_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const lat = Number(payload?.lat);\n const lon = Number(payload?.lon);\n const speed = Math.max(0, Number(payload?.speed || 0));\n const cfgCenterLat = Number(process.env.CASTLE_GEOFENCE_CENTER_LAT || 41.0082);\n const cfgCenterLon = Number(process.env.CASTLE_GEOFENCE_CENTER_LON || 28.9784);\n const cfgRadiusM = Math.max(100, Number(process.env.CASTLE_GEOFENCE_RADIUS_M || 55000));\n const cfgSpeedCap = Math.max(1, Number(process.env.CASTLE_SPEED_CAP_MPS || 35));\n const dist = Number.isFinite(lat) && Number.isFinite(lon) ? haversineMeters(cfgCenterLat, cfgCenterLon, lat, lon) : 0;\n const geofenceOk = !Number.isFinite(dist) ? true : dist <= cfgRadiusM;\n const speedOk = speed <= cfgSpeedCap;\n const failsafe = !(geofenceOk && speedOk);\n const safety = {\n geofenceOk,\n speedOk,\n failsafe,\n distanceFromCenterM: Number.isFinite(dist) ? dist : null,\n speedCapMps: cfgSpeedCap\n };\n const row = await appendTelemetry(auth.uid, deviceId, {\n lat,\n lon,\n speed,\n battery: payload?.battery,\n mode: payload?.mode || (failsafe ? \"FAILSAFE_HOLD\" : \"ACTIVE\"),\n raw: payload?.raw || {}\n });\n await updateDevice(auth.uid, deviceId, {\n status: failsafe ? \"failsafe\" : \"active\",\n lastTelemetryAt: row.ts,\n safetyState: safety\n });\n if (failsafe) {\n await addTranscript(auth.uid, {\n source: \"robotics-guardrails\",\n eventType: \"failsafe\",\n text: `Device ${deviceId} failsafe triggered geofenceOk=${geofenceOk} speedOk=${speedOk}`,\n roomId: \"studio-main\",\n meta: { deviceId, safety }\n });\n }\n sendJson(res, 200, { ok: true, telemetry: row, safety, command: failsafe ? \"FAILSAFE_HOLD\" : \"CONTINUE\" });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"telemetry_ingest_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listTelemetry(auth.uid, 150);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createAutomation(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"automation-scheduled\",\n text: `Automation scheduled: ${row.title} at ${new Date(row.scheduleAt).toLocaleString()}`,\n roomId: \"studio-main\",\n meta: { automationId: row.id, action: row.action, deviceId: row.deviceId }\n });\n sendJson(res, 200, { ok: true, automation: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"automation_create_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAutomations(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listDevices(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const devs = await listDevices(auth.uid, 500);\n const d = devs.find((x) => x.id === deviceId);\n if (!d) return sendJson(res, 404, { ok: false, error: \"device_not_found\" });\n const action = String(payload?.action || \"\");\n const caps = Array.isArray(d.capabilityProfile) ? d.capabilityProfile : [];\n if (action && caps.length && !caps.includes(action) && !caps.includes(\"*\")) {\n return sendJson(res, 400, { ok: false, error: \"unsupported_action_for_device\", capabilityProfile: caps });\n }\n const cmd = await enqueueCommand(auth.uid, {\n deviceId,\n action: action || \"NOOP\",\n params: payload?.params || {},\n adapter: payload?.adapter || d.adapter || \"websocket\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-command-queue\",\n eventType: \"command-queued\",\n text: `${cmd.deviceId} -> ${cmd.action}`,\n roomId: \"studio-main\",\n meta: { commandId: cmd.id, adapter: cmd.adapter }\n });\n sendJson(res, 200, { ok: true, command: cmd });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listCommands(auth.uid, 200);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/robotics/commands/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updateCommandStatus(auth.uid, id, payload?.status || \"sent\");\n sendJson(res, 200, { ok: true, command: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/rhizoh/bridge\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const result = await runRhizohBrainV2({\n message: String(payload?.message || \"robotics bridge command\"),\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n worldState: payload?.worldState || {},\n deviceState: payload?.deviceState || {},\n intent: \"robotics-mechanics\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-bridge\",\n eventType: \"robotics-command\",\n text: `Bridge -> ${String(result.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { directive: result.directive, agentId: payload?.agentId || \"\" }\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"robotics_bridge_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/planner/graph\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const graph = buildTaskActionGraph(String(payload?.task || \"\"), payload?.context || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"planner-graph\",\n text: `Planner graph built: ${graph.taskTitle} nodes=${graph.nodes.length}`,\n roomId: \"studio-main\",\n meta: { taskTitle: graph.taskTitle, nodeCount: graph.nodes.length }\n });\n sendJson(res, 200, { ok: true, graph });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"planner_graph_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerSocialChannel(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, channel: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_channel_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listSocialChannels(auth.uid, 100);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/broadcast\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const text = String(payload?.text || \"\").slice(0, 1200);\n const channels = await listSocialChannels(auth.uid, 100);\n const enabled = channels.filter((c) => c.status === \"enabled\");\n const deliveries = [];\n for (const c of enabled) {\n const out = await deliverSocialWithRetry(c, text);\n deliveries.push({ id: c.id, platform: c.platform, ...out });\n }\n const okCount = deliveries.filter((d) => d.ok).length;\n await addTranscript(auth.uid, {\n source: \"social-layer\",\n eventType: \"social-broadcast\",\n text: `Broadcast delivered ${okCount}/${enabled.length} channel(s): ${text.slice(0, 240)}`,\n roomId: \"studio-main\",\n meta: { deliveries }\n });\n sendJson(res, 200, { ok: true, sentChannels: okCount, totalChannels: enabled.length, deliveries });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_broadcast_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/ethics/programs\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, ...buildEthicsPrograms() });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands/dispatch\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const all = await listCommands(auth.uid, 200);\n const queued = all.filter((c) => String(c.status || \"queued\") === \"queued\").slice(0, 50);\n const devices = await listDevices(auth.uid, 500);\n const byId = new Map(devices.map((d) => [d.id, d]));\n const results = [];\n for (const cmd of queued) {\n const dev = byId.get(cmd.deviceId);\n if (!dev) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: \"device_not_found\" });\n continue;\n }\n try {\n const dispatched = await dispatchCommandToAdapter(dev, cmd);\n await updateCommandStatus(auth.uid, cmd.id, \"sent\");\n results.push({ id: cmd.id, ok: true, adapter: dispatched.adapter, via: dispatched.via });\n } catch (error) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: String(error?.message || \"dispatch_failed\") });\n }\n }\n await addTranscript(auth.uid, {\n source: \"robotics-command-dispatch\",\n eventType: \"command-dispatch-batch\",\n text: `Dispatch batch size=${results.length} ok=${results.filter((x) => x.ok).length}`,\n roomId: \"studio-main\",\n meta: { results: results.slice(0, 30) }\n });\n sendJson(res, 200, { ok: true, processed: results.length, results });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"dispatch_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/pdf/jobs/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n const job = await getPdfJob(auth.uid, id);\n if (!job) return sendJson(res, 404, { ok: false, error: \"pdf_job_not_found\" });\n sendJson(res, 200, { ok: true, job });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const row = await appendMemory({\n scope,\n id,\n text: payload?.text,\n tags: payload?.tags || [],\n importance: payload?.importance,\n kind: payload?.kind || \"episodic\",\n meta: { uid: auth.uid, ...(payload?.meta || {}) }\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.externalTruth) {\n try {\n if (process.env.RHIZOH_EXTERNAL_TRUTH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_truth:${ip}`, RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n sendJson(res, 200, buildRhizohExternalGroundTruthPayload(req));\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.productOutcome) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const rawBody = await readHttpBodyText(req, 64 * 1024);\n const body = rawBody.trim() ? safeJsonParse(rawBody) : null;\n if (!body || typeof body !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"invalid_json_payload\" });\n return;\n }\n const sigHdr = String(req.headers?.[\"x-rhizoh-outcome-signature\"] || \"\").trim();\n const hmacV = verifyRhizohOutcomeHmac(rawBody, sigHdr);\n if (!hmacV.ok) {\n sendJson(res, 401, { ok: false, error: hmacV.error || \"outcome_hmac_invalid\" });\n return;\n }\n const srcTokHdr = String(req.headers?.[\"x-rhizoh-outcome-source-token\"] || \"\").trim();\n const srcV = verifyRhizohOutcomeSourceToken(body.source, srcTokHdr);\n if (!srcV.ok) {\n sendJson(res, 401, { ok: false, error: srcV.error || \"outcome_source_token_invalid\" });\n return;\n }\n const subjectKey = String(body.subjectKey || \"\").trim().slice(0, 128);\n if (\n subjectKey &&\n !checkHttpRateLimit(\n `rhizoh_product_outcome_subj:${subjectKey}`,\n RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN,\n 60_000\n )\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_subject\" });\n return;\n }\n const result = ingestRhizohProductOutcomeHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n schemaVersion: \"1.1.0\",\n aggregate: result.aggregate,\n integrity: result.integrity\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"outcome_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.productOutcomeAggregate) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome_agg:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const u = new URL(String(req.url || \"/\"), \"http://localhost\");\n const cohortId = u.searchParams.get(\"cohortId\") || \"\";\n const decisionFingerprint = u.searchParams.get(\"decisionFingerprint\") || \"\";\n const limit = u.searchParams.get(\"limit\") || \"50\";\n const rows = listRhizohProductOutcomeAggregates({\n cohortId: cohortId || undefined,\n decisionFingerprint: decisionFingerprint || undefined,\n limit: Number(limit) || 50\n });\n sendJson(res, 200, { ok: true, schemaVersion: \"1.1.0\", rows });\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.externalLossBatch) {\n try {\n if (process.env.RHIZOH_EXTERNAL_LOSS_BATCH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_loss_batch:${ip}`, RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const result = ingestRhizohExternalLossBatchHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, { ok: true, accepted: result.accepted ?? 0 });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"batch_failed\") });\n }\n return;\n }\n\n function moderationAdminOk(req) {\n const expected = String(process.env.CASTLE_MODERATION_ADMIN_KEY || \"\").trim();\n if (!expected) return false;\n const got = String(req.headers[\"x-castle-moderation-key\"] || \"\").trim();\n return got.length > 0 && got === expected;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.opsAbuseReport) {\n try {\n const body = await readHttpJson(req, 32 * 1024);\n const auth = await resolveHttpUser(req);\n const entry = submitAbuseReportV0({\n reporterUid: auth.ok ? auth.uid : null,\n targetUid: body?.targetUid ?? null,\n category: body?.category,\n detail: body?.detail,\n traceId: body?.traceId ?? null\n });\n sendJson(res, 202, { ok: true, reportId: entry.id, status: entry.status });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsModerationQueue) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n queue: listModerationQueueV0(100),\n softBlocked: listSoftBlockedUsersV0()\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-block\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const out = softBlockUserV0(body?.uid, body?.reason);\n sendJson(res, out.ok ? 200 : 400, out);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-unblock\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n sendJson(res, 200, removeSoftBlockV0(body?.uid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsAgentSnapshots) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n const limit = Number(new URL(req.url || \"\", \"http://local\").searchParams.get(\"limit\") || 50);\n sendJson(res, 200, { ok: true, snapshots: listRecentAgentSnapshotsV0(limit) });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.academicObservatoryExport) {\n const auth = await resolveHttpUser(req);\n const out = handleAcademicObservatoryExportGetV0(req, auth);\n sendJson(res, out.status, out.body);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsHardeningStatus) {\n const auth = await resolveHttpUser(req);\n const principal = auth.ok ? `uid:${auth.uid}` : null;\n const gclSnap = await getGlobalCostLedgerSnapshotV0(principal);\n const economicStrategy = evaluateEconomicStrategyL3V0(gclSnap);\n const hardeningTenantId = resolveHardeningTenantIdV0(auth, req);\n const unifiedState = await buildUnifiedStateNarrativeV0({\n tenantId: hardeningTenantId ?? undefined,\n platformScope: !hardeningTenantId,\n principal\n });\n sendJson(res, 200, {\n ok: true,\n schema: \"rhizoh.ops.hardening_status.v0\",\n containment: readAgentContainmentConfigV0(),\n cost: readCostContainmentConfigV0(),\n gcl: {\n backend: await getGclBackendStatusV0(),\n snapshot: gclSnap\n },\n economicStrategyL3: economicStrategy,\n phasedRollout: await getPhasedRolloutStatsV0(),\n tier: readPhasedRolloutTierV0(),\n principalCost: auth.ok ? await getCostContainmentStatsV0(principal) : null,\n stressTaxonomy: verifyStressTaxonomyCoverageV0(),\n stressConflictResolution: verifyStressConflictResolutionV0(),\n resolutionStabilityEnvelope: verifyResolutionStabilityEnvelopeV0(),\n unifiedState: {\n tenantScope: {\n tenantId: unifiedState.tenantScope?.tenantId,\n scope: unifiedState.tenantScope?.scope,\n noGlobalDerivedState: unifiedState.tenantScope?.noGlobalDerivedState\n },\n narrativeFingerprint: unifiedState.narrativeFingerprint,\n screenshotScopeWatermark: unifiedState.screenshotScopeWatermark,\n interpretationSafetyContract: unifiedState.interpretationSafetyContract,\n interpretationUxContract: {\n schema: unifiedState.interpretationUxContract?.schema,\n layerOrder: unifiedState.interpretationUxContract?.layerOrder,\n narrativeNeverVisuallyDominant: unifiedState.interpretationUxContract?.narrativeNeverVisuallyDominant\n },\n governance: {\n narrativeIsNotDecisionLayer: unifiedState.governance.narrativeIsNotDecisionLayer,\n displayOrder: unifiedState.governance.displayOrder,\n trustPosture: unifiedState.governance.trustPosture,\n interpretationUxContract: unifiedState.governance.interpretationUxContract?.schema\n },\n humanOps: {\n decisionTier: unifiedState.humanOps?.humanDecisionScaling?.tier,\n decisionOwner: unifiedState.humanOps?.humanDecisionScaling?.decisionOwner,\n opsRouteId: unifiedState.humanOps?.humanDecisionOpsRunbook?.routing?.routeId,\n slaAckMinutes: unifiedState.humanOps?.humanDecisionOpsRunbook?.sla?.ackMinutes,\n uiDisclaimerRequired: unifiedState.humanOps?.narrativeUiSafety?.displayRules?.requireDisclaimerBanner,\n misreadHighResidual: unifiedState.humanOps?.misreadSimulation?.highResidualCount,\n propagationHighResidual: unifiedState.humanOps?.socialPropagationSimulation?.highResidualCount,\n worstWatermarkSurvivability:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.worstWatermarkSurvivability,\n dominantDistortionSource:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.dominantDistortionSource\n },\n culturalRisk: {\n perceptualRiskClass: unifiedState.culturalRisk?.perceptualRiskClass,\n trustFork: unifiedState.culturalRisk?.trustDynamics?.fork,\n trustDecayScore: unifiedState.culturalRisk?.trustDynamics?.scores?.trustDecay,\n mythologyScore: unifiedState.culturalRisk?.trustDynamics?.scores?.mythology,\n confidenceDetachmentExposure:\n unifiedState.culturalRisk?.trustDynamics?.axes?.confidenceDetachmentExposure\n },\n appliedSystemsLayer: {\n schema: unifiedState.appliedSystemsLayer?.schema,\n researchOutputType: unifiedState.appliedSystemsLayer?.researchGovernance?.outputType,\n roboticsBlockActuation: unifiedState.appliedSystemsLayer?.roboticsGrounding?.blockActuation,\n spiralRumorCount:\n unifiedState.appliedSystemsLayer?.spiralMMOGameKernel?.gameLayer?.rumors?.length\n },\n actionSemanticGovernance: {\n schema: unifiedState.actionSemanticGovernance?.schema,\n highRiskActionCount: unifiedState.actionSemanticGovernance?.highRiskActionCount,\n loopClosed: unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.loopClosed,\n authorityCollapseMitigated:\n unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.risks?.authorityCollapse\n ?.mitigated\n },\n actionContextResolution: {\n schema: unifiedState.actionContextResolution?.schema,\n contextFingerprint: unifiedState.actionContextResolution?.contextFingerprint?.fingerprintHash,\n domainCollision: unifiedState.actionContextResolution?.domainCollision?.detected,\n deployAgentSpiralBlocked:\n unifiedState.actionContextResolution?.actionOverloadingRisk?.deployAgentExample\n ?.spiral_mmo === \"blocked\",\n roboticsActuationEligible:\n unifiedState.actionContextResolution?.roboticsActuationCorrectness?.actuationEligible\n },\n epistemicCoherence: {\n schema: unifiedState.epistemicCoherence?.schema,\n verdict: unifiedState.epistemicCoherence?.systemCoherence?.verdict,\n coherenceScore: unifiedState.epistemicCoherence?.systemCoherence?.score,\n contradictionCount: unifiedState.epistemicCoherence?.crossLayerContradictions?.count,\n operatorDecision: unifiedState.epistemicCoherence?.uxCompression?.operatorDecision,\n headlineTr: unifiedState.epistemicCoherence?.uxCompression?.headline?.tr,\n falseSafetyWarning: Boolean(\n unifiedState.epistemicCoherence?.uxCompression?.falseSafetyWarning\n )\n },\n coherenceAuthorityBoundary: {\n schema: unifiedState.coherenceAuthorityBoundary?.schema,\n bindingValid: unifiedState.coherenceAuthorityBoundary?.eclAuthorityBinding?.valid,\n canExecute: unifiedState.coherenceAuthorityBoundary?.contract?.can_execute,\n mandatoryBannerTr:\n unifiedState.coherenceAuthorityBoundary?.disclaimers?.mandatoryBanner?.tr\n },\n decisionLatencyGovernance: {\n schema: unifiedState.decisionLatencyGovernance?.schema,\n latencyTier: unifiedState.decisionLatencyGovernance?.latencyTier,\n inflationScore: unifiedState.decisionLatencyGovernance?.latencyInflation?.score,\n fastPathEligible: unifiedState.decisionLatencyGovernance?.fastPath?.eligible,\n primaryActionId: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.primaryActionId,\n ackSlaMinutes: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.ackSlaMinutes,\n certaintyCap: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.certaintyCap,\n uncertaintyItems:\n unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.uncertaintyEnvelope?.itemCount\n },\n decisionPacketUncertaintyBoundary: {\n schema: unifiedState.decisionPacketUncertaintyBoundary?.schema,\n invariantValid: unifiedState.decisionPacketUncertaintyBoundary?.invariantCheck?.valid,\n centralInvariant: unifiedState.decisionPacketUncertaintyBoundary?.centralInvariant\n },\n epistemicDecisionPacing: {\n schema: unifiedState.epistemicDecisionPacing?.schema,\n queueSaturationTier:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation?.tier,\n operatorProcessingLatency:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation\n ?.operatorProcessingLatency,\n postureWindowCount:\n unifiedState.epistemicDecisionPacing?.temporaryStaticPostureWindows\n ?.temporaryStaticPostureWindows?.windowCount,\n executionBoundariesValid:\n unifiedState.epistemicDecisionPacing?.executionBoundaries?.valid,\n grantsExecutionApproval: unifiedState.epistemicDecisionPacing?.grantsExecutionApproval\n },\n epistemicTemporalCoherence: {\n schema: unifiedState.epistemicTemporalCoherence?.schema,\n allInvariantsValid: unifiedState.epistemicTemporalCoherence?.allInvariantsValid,\n temporalAlignmentValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.temporalAlignment?.valid,\n crossWindowGuardValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard?.valid,\n delayedTruthValid: unifiedState.epistemicTemporalCoherence?.invariants?.delayedTruthRule?.valid,\n reconciliationRequired:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard\n ?.reconciliation?.required\n },\n realityDriftObserver: {\n schema: unifiedState.realityDriftObserver?.schema,\n driftActive: unifiedState.realityDriftObserver?.driftObserverSummary?.active,\n misapprehensionShapeCount:\n unifiedState.realityDriftObserver?.misapprehensionShapeCatalog?.shapeCount,\n criticalShapeCount:\n unifiedState.realityDriftObserver?.driftObserverSummary?.criticalShapeCount,\n roboticsMismatchActive:\n unifiedState.realityDriftObserver?.roboticsFeedbackMismatch?.active\n },\n driftCausality: {\n schema: unifiedState.driftCausality?.schema,\n shapesWithCausalExplanation: unifiedState.driftCausality?.rdolLinkage?.shapesWithCausalExplanation,\n activeDomainCount: unifiedState.driftCausality?.causalitySummary?.activeDomainCount,\n propagationCausalityActive: unifiedState.driftCausality?.causalDomains?.propagation?.active\n },\n productReadiness: unifiedState.productReadiness,\n stateLayers: {\n raw: unifiedState.stateLayers.raw,\n derived: {\n systemState: unifiedState.stateLayers.derived.systemState,\n narrative: unifiedState.stateLayers.derived.narrative,\n validation: unifiedState.stateLayers.derived.validation\n },\n policy: {\n binding: unifiedState.stateLayers.policy.binding,\n decisionOwner: unifiedState.stateLayers.policy.decisionOwner,\n prohibitedActions: unifiedState.stateLayers.policy.prohibitedActions\n }\n },\n systemState: unifiedState.systemState,\n interpretation: unifiedState.interpretation,\n validation: {\n trustPosture: unifiedState.validation.trustPosture,\n adjustedConfidence: unifiedState.validation.adjustedConfidence,\n agreement: unifiedState.validation.confidenceDecomposition.agreement,\n divergenceScore: unifiedState.validation.divergence.divergenceScore,\n validated: unifiedState.validation.divergence.validated,\n uncertaintyTags: unifiedState.validation.uncertainty.tags.map((t) => t.id),\n flags: unifiedState.validation.divergence.flags.map((f) => f.id)\n },\n compression: unifiedState.compression\n }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 6 * 1024 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (\n !checkHttpRateLimit(`rhizoh_voice_transcribe:${rlKey}`, RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN, 60_000)\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const voiceEnv = rhizohVoiceTranscribeEnvV3();\n if (!voiceEnv.any) {\n sendJson(res, 503, {\n ok: false,\n error: \"voice_asr_not_configured\",\n env: voiceEnv\n });\n return;\n }\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n const result = await runRhizohVoiceTranscribeV3(\n applyLanguagePropagationToVoiceBodyV1(payload, langProp)\n );\n sendJson(res, result.ok ? 200 : 422, {\n ...result,\n traceId: String(payload?.traceId || \"\"),\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n atMs: Date.now()\n });\n } catch (error) {\n sendJson(res, 500, {\n ok: false,\n error: \"voice_transcribe_failed\",\n detail: String(error?.message || error),\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(\"/rhizoh/llm/task/\")) {\n try {\n const rawTaskId = decodeURIComponent(pathname.slice(\"/rhizoh/llm/task/\".length).split(\"/\")[0] || \"\");\n const taskId = String(rawTaskId || \"\").toLowerCase() === \"last\" ? getLatestLlmWorkerTaskIdV0() : rawTaskId;\n const snap = taskId ? getLlmWorkerTaskSnapshotV0(taskId) : null;\n if (!snap) {\n sendJson(res, 404, {\n ok: false,\n error: \"llm_task_not_found\",\n taskId: rawTaskId,\n status: \"missing\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n if (snap.status === \"processing\") {\n sendJson(res, 202, {\n ok: true,\n taskId: snap.taskId,\n status: \"processing\",\n createdAt: snap.createdAt,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n // Terminal states (completed/failed) always HTTP 200 — poll must not look like server fault (500).\n sendJson(res, 200, {\n ...(snap.body || { ok: false, error: \"llm_task_empty\", taskId: snap.taskId }),\n taskId: snap.taskId,\n status: snap.status === \"completed\" ? \"completed\" : snap.status,\n terminal: true,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n } catch (error) {\n console.error(\"[llm-task-poll] handler failed:\", error);\n sendJson(res, 200, {\n ok: false,\n error: \"llm_task_poll_handler_failed\",\n detail: String(error?.message || error),\n status: \"failed\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === rhizohRuntime.routes.rhizohLlm) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 128 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n logRhizohHealth(\"gateway_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: String(payload?.traceId || \"\"),\n auth: auth.ok ? \"ok\" : \"anon\",\n ip\n });\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (!checkHttpRateLimit(`rhizoh_llm:${rlKey}`, RL_RHIZOH_LLM_PER_MIN, 60_000)) {\n const rlTaxonomy = classifyStressResponseV0({ code: \"rate_limit_exceeded\" });\n return sendJson(res, 429, {\n ok: false,\n error: \"rate_limit_exceeded\",\n reply: rlTaxonomy.userMessageTr || \"İstek sınırı aşıldı. Kısa süre sonra tekrar deneyin.\",\n stressClass: rlTaxonomy.stressClass,\n responseAction: rlTaxonomy.responseAction,\n stressMatrix: rlTaxonomy.matrix,\n stressInterpretable: rlTaxonomy.interpretable,\n stressConfidence: rlTaxonomy.stressConfidence,\n actionConfidence: rlTaxonomy.actionConfidence,\n actionSoftened: rlTaxonomy.actionSoftened,\n responseActionStrict: rlTaxonomy.responseActionStrict,\n actionInterpretable: rlTaxonomy.actionInterpretable,\n conflictResolution: rlTaxonomy.conflictResolution,\n directive: \"NONE\"\n });\n }\n\n const requireExplicit = process.env.CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE === \"1\";\n const keyMode = normalizeClientLlmKeySource(payload?.llmKeySource ?? payload?.keySource);\n if (keyMode === null) {\n return sendJson(res, 400, {\n ok: false,\n error: \"invalid_llm_key_source\",\n reply: \"llmKeySource geçersiz. Kullanın: env | user_connection | auto\",\n directive: \"NONE\"\n });\n }\n if (requireExplicit && keyMode === \"auto\") {\n return sendJson(res, 400, {\n ok: false,\n error: \"llm_key_source_required\",\n reply: \"Sunucu CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE=1: istekte llmKeySource (env veya user_connection) zorunlu.\",\n directive: \"NONE\"\n });\n }\n\n const conn = auth.ok ? await resolveConnection(auth.uid, String(payload?.connectionId || \"\")) : null;\n\n if (keyMode === \"user_connection\") {\n if (!auth.ok) {\n return sendJson(res, 401, {\n ok: false,\n error: \"llm_auth_required_for_user_connection\",\n reply: \"Kayıtlı LLM bağlantısı için giriş gerekli.\",\n directive: \"NONE\"\n });\n }\n if (!conn?.apiKey) {\n return sendJson(res, 400, {\n ok: false,\n error: \"user_llm_connection_required\",\n reply: \"Geçerli bir LLM bağlantısı yok; Studio’da bağlantı ekleyin veya llmKeySource=env kullanın.\",\n directive: \"NONE\"\n });\n }\n }\n\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n let safePayload = applyLanguagePropagationToLlmPayloadV1({ ...(payload || {}) }, langProp);\n const trimOut = trimRhizohLlmGatewayPayloadV1(safePayload);\n if (trimOut.meta.trimmed) {\n logRhizohHealth(\"llm_payload_trimmed\", {\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n bytesBefore: trimOut.meta.bytesBefore,\n bytesAfter: trimOut.meta.bytesAfter\n });\n }\n safePayload = trimOut.payload;\n delete safePayload.apiKey;\n delete safePayload.llmKeySource;\n delete safePayload.keySource;\n\n const llmMessage = String(\n safePayload?.message || safePayload?.text || payload?.message || payload?.text || \"\"\n ).trim();\n if (!llmMessage) {\n return sendJson(res, 400, {\n ok: false,\n error: \"message_required\",\n reply: \"Mesaj metni gerekli (message alanı boş).\",\n directive: \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n if (!safePayload.message) {\n safePayload = { ...safePayload, message: llmMessage };\n }\n\n let resolvedProvider = payload?.provider;\n let resolvedModel = payload?.model;\n let connApiKey = \"\";\n\n ({\n resolvedProvider,\n resolvedModel,\n connApiKey\n } = resolveServerLlmProviderV0(keyMode, conn, payload));\n\n const turnInput = {\n safePayload,\n auth,\n keyMode,\n conn,\n resolvedProvider,\n resolvedModel,\n connApiKey\n };\n const turnMeta = {\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n langEcho: buildRhizohLanguagePropagationEchoV1(langProp)\n };\n\n if (\n isLlmWorkerEnabledV0() &&\n isLlmWorkerAsyncEnabledV0() &&\n String(req.headers[\"x-rhizoh-llm-sync\"] || \"\").trim() !== \"1\"\n ) {\n const taskId = enqueueLlmWorkerTaskAsyncV0(turnInput, turnMeta);\n logRhizohHealth(\"llm_worker_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n taskId,\n traceId: String(payload?.traceId || taskId),\n keyMode,\n async: true\n });\n sendJson(res, 202, {\n ok: true,\n taskId,\n status: \"processing\",\n traceId: String(payload?.traceId || taskId),\n pollPath: `/rhizoh/llm/task/${taskId}`,\n gatewayPid: process.pid,\n ...turnMeta.langEcho\n });\n return;\n }\n\n const { result, traceId, turnLatencyMs, spinePhases, sampledTrace } = isLlmWorkerEnabledV0()\n ? await runLlmWorkerTaskV0(turnInput, turnMeta)\n : await rhizohGatewayTurn(turnInput);\n logRhizohHealth(\"llm_response\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId,\n turnLatencyMs: Number(turnLatencyMs || 0),\n keyMode\n });\n sendJson(res, 200, {\n ...result,\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n traceId,\n turnLatencyMs,\n ...(spinePhases ? { spinePhases } : {}),\n ...(sampledTrace !== undefined ? { sampledTrace } : {})\n });\n } catch (error) {\n const msg = String(error?.message || \"\");\n const code = error?.code || \"\";\n let status = 500;\n if (msg === \"payload_too_large\") status = 413;\n else if (msg === \"rate_limit_exceeded\") status = 429;\n else if (\n code === \"cost_hard_limit\" ||\n code === \"phased_rollout_capacity\" ||\n error?.containment === true ||\n String(code).startsWith(\"agent_\")\n ) {\n status = 429;\n } else if (code === \"user_soft_blocked\" || code === \"prompt_abuse_detected\") status = 403;\n else if (\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(code) ||\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(msg) ||\n msg.includes(\"missing_api_key_for_\")\n )\n status = 400;\n let rhizohFailureKind = \"provider_error\";\n if (msg === \"rate_limit_exceeded\" || status === 429) rhizohFailureKind = \"rate_limit\";\n else if (status === 403) rhizohFailureKind = \"policy_block\";\n else if (status === 400 || status === 413) rhizohFailureKind = \"client_config\";\n else if (msg.startsWith(\"provider_http_\")) {\n const st = Number(msg.replace(\"provider_http_\", \"\"));\n if (st === 408 || st === 504) rhizohFailureKind = \"timeout\";\n else rhizohFailureKind = \"provider_error\";\n } else if (/timeout|timed out|ETIMEDOUT/i.test(msg)) rhizohFailureKind = \"timeout\";\n\n const providerHttpMatch = msg.match(/^provider_http_(\\d{3})$/);\n const stressTaxonomy =\n error?.stressClass != null\n ? {\n stressClass: error.stressClass,\n responseAction: error.responseAction,\n stressMatrix: error.stressMatrix,\n stressInterpretable: error.stressInterpretable !== false,\n stressConfidence: error.stressConfidence,\n actionConfidence: error.actionConfidence,\n actionSoftened: error.actionSoftened,\n responseActionStrict: error.responseActionStrict,\n actionInterpretable: error.actionInterpretable,\n conflictResolution: error.conflictResolution,\n stressSecondary: error.stressSecondary,\n responseActions: error.responseActions\n }\n : (() => {\n const t = classifyStressResponseV0({\n code: code || msg,\n rhizohFailureKind,\n providerHttpStatus: providerHttpMatch\n ? Number(providerHttpMatch[1])\n : null\n });\n return {\n stressClass: t.stressClass,\n responseAction: t.responseAction,\n stressMatrix: t.matrix,\n stressInterpretable: t.interpretable,\n stressConfidence: t.stressConfidence,\n actionConfidence: t.actionConfidence,\n actionSoftened: t.actionSoftened,\n responseActionStrict: t.responseActionStrict,\n actionInterpretable: t.actionInterpretable,\n conflictResolution: t.conflictResolution,\n stressSecondary: t.stressSecondary,\n responseActions: t.responseActions\n };\n })();\n sendJson(res, status, {\n ok: false,\n rhizohFailureKind,\n ...(providerHttpMatch ? { providerHttpStatus: Number(providerHttpMatch[1]) } : {}),\n error: code || msg || \"rhizoh_llm_failed\",\n detail: msg || code || undefined,\n reply:\n error?.reply ||\n (status === 500 ? \"Rhizoh bağlantısı geçici olarak kesildi.\" : msg),\n directive: error?.directive || \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n ...stressTaxonomy\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicSeal) {\n try {\n if (!EPISTEMIC_SEAL_SECRET) {\n sendJson(res, 503, { ok: false, error: \"epistemic_seal_disabled\" });\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`epistemic_seal:${ip}`, RL_EPISTEMIC_SEAL_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const truth = body.truth_contract;\n if (!truth || typeof truth !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"truth_contract_required\" });\n return;\n }\n const sealBody = {\n truth_contract: truth,\n runtime_hash: String(body.runtime_hash || \"\"),\n model_route: {\n provider: body.model_route?.provider ?? null,\n model: body.model_route?.model ?? null\n },\n memory_digest: String(body.memory_digest || \"\"),\n world_snapshot_hash: String(body.world_snapshot_hash || \"\"),\n timestamp: Number(body.timestamp) || Date.now()\n };\n const canonical = canonicalEpistemicSealString(sealBody);\n const { hash, signature } = hashAndSignEpistemicSeal(canonical, EPISTEMIC_SEAL_SECRET);\n recordGenesisEpistemicSealIssued(hash);\n const attestation = {\n schema: EPISTEMIC_SEAL_SCHEMA,\n algorithm: \"SHA-256+HMAC-SHA256\",\n keyHint: process.env.CASTLE_EPISTEMIC_SEAL_SECRET ? \"CASTLE_EPISTEMIC_SEAL_SECRET\" : \"CASTLE_GATEWAY_TOKEN\",\n issuedAt: Date.now(),\n gateway: { service: \"castle-gateway\", listenPort: PORT }\n };\n sendJson(res, 200, {\n ok: true,\n hash,\n signature,\n attestation,\n canonicalBytes: Buffer.byteLength(canonical, \"utf8\")\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"seal_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicLogsBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`epistemic_logs:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 120) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const persisted = await persistEpistemicLedgerBatch(auth.uid, entries);\n recordGenesisEpistemicLedgerPersisted(persisted.persisted);\n // Internal cognition layer: forecast stays gateway/ledger-only (no client exposure).\n try {\n await persistEpistemicForecastBatch(auth.uid, persisted.normalized || []);\n } catch {\n /* forecast failures must not break primary ledger ingest */\n }\n sendJson(res, 200, {\n ok: true,\n persisted: persisted.persisted,\n mode: persisted.mode,\n latest: persisted.normalized?.[persisted.normalized.length - 1] || null\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"epistemic_log_ingest_failed\") });\n }\n return;\n }\n\n sendJson(res, 404, { ok: false, error: \"not_found\" });\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\nspiralWssForGenesis = wss;\nconst orchestrator = createOrchestrator();\nconst clientStats = new Map();\nlet broadcasterClientId = null;\nconst spiralState = {\n rooms: [{ id: \"room-default\", name: \"Spiral Default Arena\" }],\n activeByClient: new Map(),\n characters: new Map() // id -> {id,name,role,roomId,source}\n};\n\n/** Castle social rooms — keyed by `castleRoomKey` from {@link WS_MESSAGE.CASTLE_SOCIAL_PULSE}. */\nconst castleSocialRoomState = new Map();\nconst CASTLE_SOCIAL_MEMBER_TTL_MS = 90_000;\nconst CASTLE_SOCIAL_MAX_PULSE_BYTES = 8192;\nconst RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES = 5 * 1024 * 1024;\nconst RHIZOH_VOICE_LIVE_MAX_CHUNKS = 96;\n\n/**\n * Voice Engine v3 live media lane.\n * Client streams MediaRecorder chunks over the existing gateway WS; STOP reuses the\n * gateway ASR runner so orchestration remains the single authority downstream.\n */\nconst rhizohVoiceLiveSessions = new WeakMap();\n\nfunction appendRhizohVoiceLiveEventV0(session, type, payloadRefSeed = \"\") {\n if (!session) return null;\n session.eventSeq = (Number(session.eventSeq) || 0) + 1;\n const event = createRhizohImmutableEventV0({\n type,\n sessionId: session.sessionId,\n traceId: session.traceId,\n actorSeed: session.sessionId || session.traceId,\n eventSeq: session.eventSeq,\n payloadRef: createRhizohPayloadRefV0(payloadRefSeed || `${session.sessionId}:${type}:${session.eventSeq}`)\n });\n session.events = [...(session.events || []), event].slice(-64);\n return event;\n}\n\nfunction sendRhizohVoiceLiveEnvelopeV0(socket, type, payload) {\n try {\n if (socket?.readyState === 1) socket.send(JSON.stringify(createEnvelope(type, payload)));\n } catch {\n /* noop */\n }\n}\n\nfunction decodeRhizohVoiceLiveChunkV0(payload = {}) {\n const b64 = String(payload.audioBase64 || \"\").trim();\n if (!b64) return { ok: false, error: \"audio_base64_required\" };\n try {\n const buf = Buffer.from(b64, \"base64\");\n if (!buf.length) return { ok: false, error: \"audio_decode_empty\" };\n return { ok: true, buffer: buf };\n } catch {\n return { ok: false, error: \"audio_base64_invalid\" };\n }\n}\n\nfunction handleRhizohVoiceLiveStartV0(socket, payload = {}) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const sessionId = String(p.sessionId || `live_${Date.now().toString(36)}`).slice(0, 96);\n rhizohVoiceLiveSessions.set(socket, {\n sessionId,\n traceId: String(p.traceId || \"\").slice(0, 128),\n mimeType: String(p.mimeType || \"audio/webm\").slice(0, 80),\n languageCode: String(p.languageCode || \"tr-TR\").slice(0, 32),\n path: String(p.path || \"both\").slice(0, 24),\n chunks: [],\n chunkIndexes: [],\n totalBytes: 0,\n nextChunkIndex: 1,\n eventSeq: 0,\n events: [],\n startedAtMs: Date.now()\n });\n appendRhizohVoiceLiveEventV0(rhizohVoiceLiveSessions.get(socket), \"VOICE_LIVE_SESSION_STARTED\", `${sessionId}:live_session`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_START, {\n ok: true,\n sessionId,\n schema: \"castle.rhizoh.voice_live.gateway_lane.v0\"\n });\n}\n\nfunction handleRhizohVoiceLiveChunkV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const sequence = validateRhizohVoiceLiveChunkPayloadV0(payload, {\n expectedChunkIndex: session.nextChunkIndex\n });\n if (!sequence.ok) {\n rhizohVoiceLiveSessions.delete(socket);\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:chunk_reject`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: sequence.error,\n expectedChunkIndex: sequence.expectedChunkIndex,\n chunkIndex: sequence.chunkIndex\n });\n return;\n }\n const decoded = decodeRhizohVoiceLiveChunkV0(payload);\n if (!decoded.ok) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: decoded.error\n });\n return;\n }\n if (\n session.chunks.length >= RHIZOH_VOICE_LIVE_MAX_CHUNKS ||\n session.totalBytes + decoded.buffer.length > RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES\n ) {\n rhizohVoiceLiveSessions.delete(socket);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_audio_too_large\"\n });\n return;\n }\n session.chunks.push(decoded.buffer);\n session.chunkIndexes.push(sequence.chunkIndex);\n session.totalBytes += decoded.buffer.length;\n session.nextChunkIndex += 1;\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_CHUNK_ACCEPTED\", `${session.sessionId}:chunk:${sequence.chunkIndex}`);\n}\n\nfunction handleRhizohVoiceLiveStopV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n rhizohVoiceLiveSessions.delete(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const lastChunkIndex = Number(payload?.lastChunkIndex);\n if (Number.isInteger(lastChunkIndex) && lastChunkIndex !== session.chunks.length) {\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:stop_mismatch`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_chunk_count_mismatch\",\n expectedChunkCount: lastChunkIndex,\n receivedChunkCount: session.chunks.length\n });\n return;\n }\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_STOP_RECEIVED\", `${session.sessionId}:stop:${session.chunks.length}`);\n const audio = Buffer.concat(session.chunks);\n if (!audio.length) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"audio_empty\"\n });\n return;\n }\n runRhizohVoiceTranscribeV3({\n path: String(payload?.path || session.path || \"both\"),\n audioBase64: audio.toString(\"base64\"),\n mimeType: String(payload?.mimeType || session.mimeType || \"audio/webm\"),\n languageCode: String(payload?.languageCode || session.languageCode || \"tr-TR\"),\n traceId: String(payload?.traceId || session.traceId || \"\"),\n sessionId: session.sessionId,\n transport: \"gateway_ws_live\"\n })\n .then((result) => {\n appendRhizohVoiceLiveEventV0(session, result?.ok ? \"VOICE_TRANSCRIBE_FINAL\" : \"VOICE_TRANSCRIBE_FAILED\", `${session.sessionId}:final`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_FINAL, {\n ...result,\n sessionId: session.sessionId,\n transportPath: \"gateway_ws_live\",\n bytes: audio.length,\n chunkCount: session.chunks.length,\n chunkSequence: Object.freeze({\n ok: true,\n firstChunkIndex: session.chunkIndexes[0] || null,\n lastChunkIndex: session.chunkIndexes[session.chunkIndexes.length - 1] || null,\n receivedChunkCount: session.chunks.length\n }),\n eventTrace: Object.freeze((session.events || []).slice(-16)),\n recordedMs: Math.max(0, Date.now() - session.startedAtMs)\n });\n })\n .catch((e) => {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_transcribe_failed\",\n detail: String(e?.message || e)\n });\n });\n}\n\nfunction removeCastleSocialClientFromAllRooms(clientId) {\n const cid = String(clientId || \"\");\n if (!cid) return;\n for (const room of castleSocialRoomState.values()) {\n room.members.delete(cid);\n }\n}\n\nfunction buildCastleSocialRoomPayload(roomKey) {\n const room = castleSocialRoomState.get(roomKey);\n if (!room) return null;\n const now = Date.now();\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const roster = [...room.members.values()].map((m) => ({\n userId: m.userId,\n lastMs: m.lastMs,\n ...m.sp\n }));\n const peerFeeds =\n room.walPeers && room.walPeers.size\n ? [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }))\n : [];\n return { castleRoomKey: roomKey, seq: room.seq, roster, peerFeeds, ts: now };\n}\n\nconst CASTLE_WAL_PEER_MAX_FEED_BYTES = 48 * 1024;\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {{ ok: boolean, code?: string, reason?: string }} gate\n */\nfunction rejectWalPeerIngressV0(socket, gate) {\n const code = gate.code || \"wal_peer_rejected\";\n recordWalPeerRejectV0(code);\n recordSubstrateOtelEventV0(\"wal_peer_reject\", { code, reason: String(gate.reason || \"\").slice(0, 120) });\n socket.send(\n JSON.stringify(\n createEnvelope(WS_MESSAGE.ERROR, {\n error: gate.reason || \"WAL peer ingress rejected.\",\n code\n })\n )\n );\n}\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {Record<string, unknown> | null | undefined} walPeerFeed\n * @returns {boolean}\n */\nfunction gateWalPeerIngressV0(socket, walPeerFeed) {\n const authGate = assertWalPeerSocketAuthorityV0(socket);\n if (!authGate.ok) {\n rejectWalPeerIngressV0(socket, authGate);\n return false;\n }\n const sigGate = validateWalPeerFeedSignatureV0(\n walPeerFeed && typeof walPeerFeed === \"object\" ? walPeerFeed : {}\n );\n if (!sigGate.ok) {\n rejectWalPeerIngressV0(socket, sigGate);\n return false;\n }\n return true;\n}\n\nfunction handleCastleWalPeerFeed(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_WAL_PEER_MAX_FEED_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_WAL_PEER_FEED too large.\" })));\n return;\n }\n const walPeerFeed = p.walPeerFeed && typeof p.walPeerFeed === \"object\" ? p.walPeerFeed : {};\n if (!gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const castleRoomKey = String(p.castleRoomKey || \"wal:convergence\").slice(0, 64);\n const castleId = String(p.castleId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId,\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: room.members.get(socket.clientId)?.sp ?? {}\n });\n const peerFeeds = [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }));\n const body = {\n castleRoomKey,\n seq: room.seq,\n peerFeeds,\n ts: now\n };\n const env = createEnvelope(WS_MESSAGE.CASTLE_WAL_PEER_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys(), ...room.walPeers.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction handleCastleSocialPulse(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_SOCIAL_MAX_PULSE_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_SOCIAL_PULSE too large.\" })));\n return;\n }\n const castleRoomKey = String(p.castleRoomKey || \"default\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n const sp = p.socialPulse && typeof p.socialPulse === \"object\" ? p.socialPulse : {};\n const walPeerFeed =\n p.walPeerFeed && typeof p.walPeerFeed === \"object\"\n ? p.walPeerFeed\n : sp.walPeerFeed && typeof sp.walPeerFeed === \"object\"\n ? sp.walPeerFeed\n : null;\n if (walPeerFeed && !gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const spNext = walPeerFeed ? { ...sp, walPeerFeed } : { ...sp };\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: spNext\n });\n if (walPeerFeed) {\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId: String(p.castleId || userId).slice(0, 64),\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n }\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const body = buildCastleSocialRoomPayload(castleRoomKey);\n if (!body) return;\n const env = createEnvelope(WS_MESSAGE.CASTLE_SOCIAL_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcast(msgObj) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcastState() {\n broadcast(\n createEnvelope(WS_MESSAGE.BROADCAST_STATE, {\n broadcasterClientId,\n viewers: [...wss.clients].filter((c) => c.readyState === 1).length\n })\n );\n}\n\nfunction currentSpiralStatePayload() {\n return {\n rooms: spiralState.rooms,\n characters: [...spiralState.characters.values()]\n };\n}\n\nfunction broadcastSpiralState() {\n broadcast(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload()));\n}\n\nfunction ensureRoom(roomId, fallbackName = null) {\n if (!roomId) return spiralState.rooms[0].id;\n const exists = spiralState.rooms.find((r) => r.id === roomId);\n if (exists) return exists.id;\n const created = { id: roomId, name: fallbackName || `Spiral-${roomId.slice(-4)}` };\n spiralState.rooms.push(created);\n return created.id;\n}\n\nfunction syncSpiralCharactersWithWorld(snapshot) {\n const agents = snapshot?.agents || [];\n for (const a of agents) {\n if (!spiralState.characters.has(a.id)) {\n spiralState.characters.set(a.id, {\n id: a.id,\n name: a.id,\n role: \"agent\",\n roomId: spiralState.rooms[0].id,\n source: \"world\"\n });\n }\n }\n}\n\nfunction broadcastExcept(msgObj, exceptSocket) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client !== exceptSocket && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction validateInputFrame(payload, socket) {\n const stats = clientStats.get(socket.clientId) || { lastFrame: 0, windowStart: Date.now(), msgCount: 0 };\n const now = Date.now();\n if (now - stats.windowStart > 5000) {\n stats.windowStart = now;\n stats.msgCount = 0;\n }\n stats.msgCount += 1;\n clientStats.set(socket.clientId, stats);\n if (stats.msgCount > 120) return \"Rate limit exceeded.\";\n\n if (!payload || !Number.isInteger(payload.frame)) return \"frame must be integer.\";\n if (payload.frame <= stats.lastFrame) return \"frame must be monotonic.\";\n const commands = Array.isArray(payload.commands) ? payload.commands : [];\n if (commands.length > 8) return \"Too many commands in one frame.\";\n for (const cmd of commands) {\n if (!cmd || typeof cmd.command !== \"string\") return \"Invalid command format.\";\n if (\n cmd.command !== \"SPAWN_AGENT\" &&\n cmd.command !== \"LIST_CASTLES\" &&\n cmd.command !== \"SPAWN_ENTITY\" &&\n cmd.command !== \"CREATE_CASTLE\"\n ) return \"Unsupported command.\";\n }\n stats.lastFrame = payload.frame;\n clientStats.set(socket.clientId, stats);\n return null;\n}\n\nfunction validateSignalPayload(payload) {\n if (!payload || typeof payload !== \"object\") return \"Invalid signaling payload.\";\n const signalType = payload.signalType;\n if (![\"OFFER\", \"ANSWER\", \"ICE\"].includes(signalType)) return \"Invalid signal type.\";\n if (typeof payload.to !== \"string\" || payload.to.length < 3) return \"Invalid signal destination.\";\n if (signalType !== \"ICE\" && JSON.stringify(payload.sdp || {}).length > 16 * 1024) return \"SDP too large.\";\n if (signalType === \"ICE\" && JSON.stringify(payload.candidate || {}).length > 6 * 1024) return \"ICE too large.\";\n return null;\n}\n\nfunction sanitizeRawMessage(raw) {\n if (!raw) return null;\n const text = raw.toString();\n if (Buffer.byteLength(text, \"utf8\") > MAX_MESSAGE_BYTES) return null;\n return text;\n}\n\nwss.on(\"connection\", async (socket, req) => {\n const origin = req?.headers?.origin || \"\";\n if (!httpCors.isWebSocketOriginAllowed(origin)) {\n socket.close(1008, \"Origin not allowed\");\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const path = req?.url || \"\";\n const query = new URL(`ws://localhost${path}`).searchParams;\n const token = query.get(\"token\");\n if (!token || token !== REQUIRED_GATEWAY_TOKEN) {\n socket.close(1008, \"Invalid gateway token\");\n return;\n }\n }\n if (REQUIRE_AUTH) {\n const result = await verifyClientToken(req);\n if (!result.ok) {\n const isLocal = origin.includes(\"localhost\") || origin.includes(\"127.0.0.1\") || !origin;\n if (!(ALLOW_DEV_ANON && isLocal)) {\n socket.close(1008, result.reason);\n return;\n }\n socket.auth = { ok: true, kind: \"dev-anon\", user: { uid: \"dev-anon\" } };\n } else {\n socket.auth = result;\n }\n }\n\n socket.clientId = `c-${Math.random().toString(36).slice(2, 8)}`;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.HELLO, { message: \"Castle Gateway online.\", clientId: socket.clientId })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.WORLD_SNAPSHOT, orchestrator.worldSnapshot())));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.BROADCAST_STATE, { broadcasterClientId, viewers: [...wss.clients].filter((c) => c.readyState === 1).length })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload())));\n broadcastExcept(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }), socket);\n\n socket.on(\"message\", (raw) => {\n const safeRaw = sanitizeRawMessage(raw);\n if (!safeRaw) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Message too large or invalid.\" })));\n return;\n }\n const parsed = safeJsonParse(safeRaw);\n if (!parsed?.type) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid message.\" })));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND_TEXT) {\n const canonical = parseTextToCommand(parsed.payload?.text);\n if (!canonical) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Unknown command text.\" })));\n return;\n }\n const result = orchestrator.applyCommand(canonical);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND) {\n const result = orchestrator.applyCommand(parsed.payload);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n }\n\n if (parsed.type === WS_MESSAGE.INPUT_FRAME) {\n const err = validateInputFrame(parsed.payload, socket);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const frame = parsed.payload.frame;\n const commands = Array.isArray(parsed.payload?.commands) ? parsed.payload.commands : [];\n orchestrator.queueInputFrame(socket.clientId, frame, commands);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, queued: true, frame })));\n }\n\n if (parsed.type === WS_MESSAGE.SIGNAL) {\n const payload = parsed.payload || {};\n const err = validateSignalPayload(payload);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const signalType = payload.signalType;\n const to = payload.to;\n for (const client of wss.clients) {\n if (client.clientId === to && client.readyState === 1) {\n client.send(JSON.stringify(createEnvelope(WS_MESSAGE.SIGNAL, { ...payload, from: socket.clientId })));\n break;\n }\n }\n }\n\n if (parsed.type === WS_MESSAGE.DRONE_TELEMETRY) {\n const payload = parsed.payload || {};\n const json = JSON.stringify(payload);\n if (Buffer.byteLength(json, \"utf8\") > 48 * 1024) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"DRONE_TELEMETRY payload too large.\" })));\n return;\n }\n broadcastExcept(createEnvelope(WS_MESSAGE.DRONE_TELEMETRY, { ...payload, relayFrom: socket.clientId }), socket);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_START) {\n handleRhizohVoiceLiveStartV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_CHUNK) {\n handleRhizohVoiceLiveChunkV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_STOP) {\n handleRhizohVoiceLiveStopV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_SOCIAL_PULSE) {\n handleCastleSocialPulse(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_WAL_PEER_FEED) {\n handleCastleWalPeerFeed(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.OPEN_DATA_QUERY) {\n queryOpenData(parsed.payload)\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: true, result })));\n })\n .catch((e) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: false, error: e.message })));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_QUERY) {\n runRhizohBrainV2({\n ...(parsed.payload || {}),\n uid: socket?.auth?.user?.uid || \"unknown\"\n })\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, result)));\n })\n .catch(async () => {\n const fallback = await runRhizohBrain(parsed.payload || {}).catch(() => ({\n ok: false,\n reply: \"Rhizoh temporary disturbance. Habitat remains stable.\"\n }));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, fallback)));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.BROADCAST_REGISTER) {\n const role = String(parsed.payload?.role || \"\");\n if (role === \"GENESIS_BROADCAST_AGENT\") {\n broadcasterClientId = socket.clientId;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, registered: role })));\n broadcastState();\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.STUDIO_CUE) {\n // Only registered broadcaster can push global cues.\n if (!broadcasterClientId || socket.clientId !== broadcasterClientId) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Only broadcaster agent can emit STUDIO_CUE.\" })));\n return;\n }\n const cue = parsed.payload || {};\n if (typeof cue !== \"object\") {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid STUDIO_CUE payload.\" })));\n return;\n }\n broadcast(createEnvelope(WS_MESSAGE.STUDIO_CUE, cue));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_JOIN_ROOM) {\n const requested = String(parsed.payload?.roomId || \"\").trim();\n const requestedName = String(parsed.payload?.roomName || \"\").trim();\n const roomId = ensureRoom(requested || `room-${Math.random().toString(36).slice(2, 7)}`, requestedName || null);\n spiralState.activeByClient.set(socket.clientId, roomId);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, roomId })));\n broadcastSpiralState();\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_CREATE_CHARACTER) {\n const name = String(parsed.payload?.name || \"\").trim().slice(0, 32) || `AI-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;\n const role = String(parsed.payload?.role || \"agent\").trim().slice(0, 24) || \"agent\";\n const roomId = ensureRoom(\n String(parsed.payload?.roomId || spiralState.activeByClient.get(socket.clientId) || spiralState.rooms[0].id),\n null\n );\n\n // Character creation always maps to an authoritative world agent.\n const result = orchestrator.applyCommand({\n command: COMMAND.SPAWN_AGENT,\n params: { targetCastleId: \"castle-01\" }\n });\n if (!result?.ok || !result?.data?.id) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Character creation failed.\" })));\n return;\n }\n const id = result.data.id;\n spiralState.characters.set(id, { id, name, role, roomId, source: \"spawned\" });\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, characterId: id, roomId })));\n broadcastSpiralState();\n return;\n }\n });\n\n socket.on(\"close\", () => {\n if (socket.clientId === broadcasterClientId) broadcasterClientId = null;\n clientStats.delete(socket.clientId);\n rhizohVoiceLiveSessions.delete(socket);\n spiralState.activeByClient.delete(socket.clientId);\n removeCastleSocialClientFromAllRooms(socket.clientId);\n broadcast(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }));\n broadcastState();\n });\n});\n\nsetInterval(() => {\n void (async () => {\n try {\n const { frame, snapshot, delta, packedDelta } = orchestrator.tick(1 / 20);\n syncSpiralCharactersWithWorld(snapshot);\n broadcast(createEnvelope(WS_MESSAGE.NETWORK_TICK, { frame }));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_TICK, snapshot));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA, delta));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA_PACKED, packedDelta));\n broadcastSpiralState();\n await runAcademyEventTick();\n const executed = await tickAutomations(Date.now());\n for (const a of executed) {\n await addTranscript(a.uid, {\n source: \"planner\",\n eventType: \"automation-executed\",\n text: `Automation executed: ${a.title} (${a.action})`,\n roomId: \"studio-main\",\n meta: { automationId: a.id, action: a.action, deviceId: a.deviceId || \"\" }\n });\n }\n } catch (e) {\n console.error(\"[GATEWAY] world tick / automations loop error:\", String(e?.message || e));\n }\n })();\n}, 50);\n\n(async () => {\n await rhizohRuntime.telemetry.initOpenTelemetry();\n if (String(process.env.CASTLE_GENESIS_DISK_PERSIST ?? \"\").trim() === \"\") {\n process.env.CASTLE_GENESIS_DISK_PERSIST = \"1\";\n }\n const genesisDiskBoot = await hydrateGenesisContinuityPersistenceBootV0();\n if (!genesisDiskBoot.ok) {\n console.warn(\"[GATEWAY] genesis disk hydrate failed; continuing with in-memory genesis continuity\");\n }\n httpServer.listen(PORT, () => {\n startGenesisCanonicalClock();\n installGenesisCheckpointSurfaceGetter(buildGenesisRuntimeSurfacePayloadLive);\n setGenesisContinuityAfterPublishHook((seq) => {\n noteGenesisCheckpointSeqCommitted(seq);\n noteGenesisContinuitySeqAcceptedV0(seq);\n });\n startGenesisContinuityInfraSampler(buildGenesisRuntimeSurfacePayloadLive, 2000);\n logProductionObservatorySurfaceGuardsV0();\n logGatewaySubstrateAuthorityGuardsV0();\n if (isLlmWorkerEnabledV0()) {\n try {\n const workerBoot = initLlmWorkerAtBootV0();\n console.log(\"[GATEWAY] llm worker boot:\", workerBoot);\n } catch (workerErr) {\n console.error(\"[GATEWAY] llm worker boot failed:\", workerErr);\n }\n }\n console.log(`[GATEWAY] ws/http://localhost:${PORT}`);\n console.log(\n `[GATEWAY] genesis continuity: GET ${rhizohRuntime.routes.genesisRuntime} | POST ${rhizohRuntime.routes.genesisIngress} | SSE ${rhizohRuntime.routes.genesisStream} | replay ${rhizohRuntime.routes.genesisReplay} | diff ${rhizohRuntime.routes.genesisReplayDiff} | equiv ${rhizohRuntime.routes.genesisReplayEquivalence} | analytics ${rhizohRuntime.routes.genesisReplayAnalytics} | evolution ${rhizohRuntime.routes.genesisReplayEvolution}`\n );\n if (String(process.env.CASTLE_GENESIS_OBSERVATORY_LIVE_V01 ?? \"\").trim() === \"1\") {\n console.log(\"GENESIS OBSERVATORY LIVE v0.1 — READ ONLY PHASE STARTED\");\n }\n });\n})().catch((e) => {\n console.error(\"[GATEWAY] fatal boot (before or during listen):\", e);\n process.exit(1);\n});\n"}, "3cad809c279c81eb86d02804f7564f5528459bc8": {"t": 1785754139.6034868, "n": 169592, "h": "236185c610967a58", "c": "import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport dotenv from \"dotenv\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconsole.log(\"🔥 [GENESIS_BOOT] entry file:\", __filename);\nconsole.log(\"🔥 [GENESIS_BOOT] node env:\", process.env.NODE_ENV);\nconsole.log(\"🔥 [GENESIS_BOOT] pid:\", process.pid);\nconsole.log(\"🧬 [GENESIS_BOOT] cwd:\", process.cwd());\nconsole.log(\"🧬 [GENESIS_BOOT] import.meta.url:\", import.meta.url);\ndotenv.config({ path: path.join(__dirname, \"..\", \".env\") });\ndotenv.config({ path: path.join(__dirname, \"..\", \".env.local\"), override: true });\n\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport { createServer } from \"node:http\";\nimport { createOrchestrator } from \"@castle/orchestrator\";\nimport { parseTextToCommand } from \"@castle/command-dsl\";\nimport {\n WS_MESSAGE,\n COMMAND,\n createEnvelope,\n createRhizohImmutableEventV0,\n createRhizohPayloadRefV0,\n safeJsonParse,\n validateRhizohVoiceLiveChunkPayloadV0\n} from \"@castle/protocol\";\nimport { queryOpenData } from \"./openData.js\";\nimport { verifyClientToken } from \"./auth.js\";\nimport { runRhizohBrain } from \"./rhizohBrain.js\";\nimport { queryRhizohLlm } from \"./rhizohLlmGateway.js\";\nimport { rhizohGatewayTurn } from \"./rhizohGatewayTurn.js\";\nimport {\n enqueueLlmWorkerTaskAsyncV0,\n getLatestLlmWorkerTaskIdV0,\n getLlmWorkerDebugSnapshotV0,\n getLlmWorkerTaskSnapshotV0,\n initLlmWorkerAtBootV0,\n isLlmWorkerAsyncEnabledV0,\n isLlmWorkerEnabledV0,\n runLlmWorkerTaskV0\n} from \"./llmWorkerRuntime.js\";\nimport {\n RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n runRhizohVoiceTranscribeV3,\n rhizohVoiceTranscribeEnvV3\n} from \"./rhizohVoiceTranscribeV3.js\";\nimport { createHttpCorsPolicy, normalizeHttpOrigin, parseAllowedOriginsFromEnv } from \"./httpCorsPolicyV1.js\";\nimport {\n applyLanguagePropagationToLlmPayloadV1,\n applyLanguagePropagationToVoiceBodyV1,\n buildRhizohLanguagePropagationEchoV1,\n parseRhizohLanguagePropagationV1\n} from \"./rhizohLanguagePropagationV1.js\";\nimport { trimRhizohLlmGatewayPayloadV1 } from \"./rhizohLlmPayloadTrimV1.js\";\nimport {\n meshAppendDelta,\n meshContinuityAggregate,\n meshJoin,\n meshLeave,\n meshReplayFromNodeId,\n meshReplayFromSeq,\n meshSnapshot,\n meshSubscribeSse\n} from \"./presenceRoomMesh.js\";\nimport { runRhizohBrainV2 } from \"./rhizohBrainV2.js\";\nimport {\n listConnections,\n createConnection,\n updateConnection,\n deleteConnection,\n setDefaultConnection,\n resolveConnection\n} from \"./llmConnectionsStore.js\";\nimport { checkHttpRateLimit, getHttpClientIp } from \"./castleHttpRateLimit.js\";\nimport {\n submitAbuseReportV0,\n listModerationQueueV0,\n softBlockUserV0,\n removeSoftBlockV0,\n listSoftBlockedUsersV0\n} from \"./ops/moderationMvpV0.js\";\nimport { listRecentAgentSnapshotsV0 } from \"./ops/agentObservabilityV0.js\";\nimport { readAgentContainmentConfigV0 } from \"./ops/agentContainmentV0.js\";\nimport { readCostContainmentConfigV0, getCostContainmentStatsV0 } from \"./ops/costContainmentV0.js\";\nimport { getGlobalCostLedgerSnapshotV0, getGclBackendStatusV0 } from \"./ops/globalCostLedgerV0.js\";\nimport { evaluateEconomicStrategyL3V0 } from \"./ops/economicStrategyEngineL3V0.js\";\nimport { getPhasedRolloutStatsV0, readPhasedRolloutTierV0 } from \"./ops/phasedRolloutV0.js\";\nimport { buildUnifiedStateNarrativeV0 } from \"./ops/unifiedStateNarrativeV0.js\";\nimport { resolveHardeningTenantIdV0 } from \"./ops/narrativeTenantIsolationV0.js\";\nimport {\n classifyStressResponseV0,\n verifyStressTaxonomyCoverageV0,\n verifyStressConflictResolutionV0\n} from \"./ops/stressResponseTaxonomyV0.js\";\nimport { verifyResolutionStabilityEnvelopeV0 } from \"./ops/resolutionStabilityEnvelopeV0.js\";\nimport { logLlmAccess } from \"./castleLlmAudit.js\";\nimport { appendMemory, listMemories, getMemoryContext, getPersonaGoalMemory, setPersonaGoalMemory, autoCompactMemories } from \"./memoryStore.js\";\nimport { getFirebasePersistence } from \"./firebasePersistence.js\";\nimport { registerAgentIdentity, listAgentIdentities, getAgentIdentity, updateAgentIdentity } from \"./agentIdentityStore.js\";\nimport { queueAcademyEvent, listAcademyEvents, resolveAcademyEvent, runAcademyEventTick } from \"./academyEventEngine.js\";\nimport { enqueuePdfJob, getPdfJob } from \"./pdfPipeline.js\";\nimport {\n createPublishSession,\n listPublishSessions,\n updatePublishSession,\n addTranscript,\n listTranscripts,\n searchMetadata,\n registerDevice,\n listDevices,\n updateDevice,\n appendTelemetry,\n listTelemetry,\n createAutomation,\n listAutomations,\n tickAutomations,\n enqueueCommand,\n listCommands,\n updateCommandStatus,\n registerSocialChannel,\n listSocialChannels\n} from \"./studioOpsStore.js\";\nimport { metrics as infraMetrics } from \"./infra/metrics.js\";\nimport { renderRhizohPrometheusMetrics, rhizohEnterpriseMetrics } from \"./infra/rhizohEnterpriseMetrics.js\";\nimport { scoreHealth } from \"./infra/healthScore.js\";\nimport { getTraceById, listTracesBySession } from \"./infra/traceRegistry.js\";\nimport { canonicalEpistemicSealString, hashAndSignEpistemicSeal, EPISTEMIC_SEAL_SCHEMA } from \"./epistemicSeal.js\";\nimport { persistEpistemicLedgerBatch } from \"./epistemicLedgerStore.js\";\nimport { persistEpistemicForecastBatch } from \"./epistemicForecastStore.js\";\nimport { buildRhizohExternalGroundTruthPayload } from \"./rhizohExternalGroundTruthGateway.js\";\nimport { ingestRhizohExternalLossBatchHttp } from \"./rhizohExternalLossIngestGateway.js\";\nimport {\n ingestRhizohProductOutcomeHttp,\n listRhizohProductOutcomeAggregates,\n verifyRhizohOutcomeHmac,\n verifyRhizohOutcomeSourceToken\n} from \"./rhizohProductOutcomeIngestGateway.js\";\nimport { initRhizoh } from \"./rhizohProductionBootstrap.js\";\nimport { handleAcademicObservatoryExportGetV0 } from \"./rhizoh/academicObservatoryHttpV0.js\";\nimport {\n handleLiveSportsBundleGetV0,\n handleLiveNewsHeadlinesGetV0,\n handleLiveWorldFeedGetV0\n} from \"./rhizoh/liveWorldFeedHttpV0.js\";\nimport {\n buildGenesisRuntimeSurfacePayload,\n recordGenesisEpistemicLedgerPersisted,\n recordGenesisEpistemicSealIssued,\n startGenesisCanonicalClock\n} from \"./genesisRuntimeSurfaceV0.js\";\nimport { registerGenesisSseClient, getGenesisContinuitySeq, setGenesisContinuityAfterPublishHook } from \"./genesisContinuityStreamHubV0.js\";\nimport { computeGenesisReplayFingerprintV0 } from \"./genesisReplayFingerprintV0.js\";\nimport { buildGenesisGatewayCapabilitiesV0 } from \"./genesisGatewayCapabilitiesV0.js\";\nimport {\n installGenesisCheckpointSurfaceGetter,\n noteGenesisCheckpointSeqCommitted,\n getLatestGenesisSignedCheckpoint\n} from \"./genesisContinuityCheckpointV0.js\";\nimport { hydrateGenesisContinuityPersistenceBootV0 } from \"./genesisContinuityHydrateBootV0.js\";\nimport { startGenesisContinuityInfraSampler } from \"./genesisContinuityInfraSamplerV0.js\";\nimport {\n genesisCheckpointQueryBySeqV0,\n genesisCheckpointQueryRangeV0,\n genesisCheckpointQueryLineageV0\n} from \"./genesisCheckpointQueryV0.js\";\nimport {\n genesisContinuityEventArchiveEnabled,\n queryGenesisContinuityEventArchiveV0,\n GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION\n} from \"./genesisContinuityEventArchiveV0.js\";\nimport { resolveGenesisReplayRouterV1, GENESIS_REPLAY_ROUTER_SCHEMA } from \"./genesisReplayRouterV1.js\";\nimport {\n computeGenesisReplayTemporalDiffV1,\n GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA\n} from \"./genesisReplayTemporalDiffV1.js\";\nimport { compareGenesisReplayEquivalenceV1 } from \"./genesisReplayEquivalenceV1.js\";\nimport { computeGenesisReplayAnalyticsV1 } from \"./genesisReplayAnalyticsV1.js\";\nimport { computeGenesisReplayEvolutionV1 } from \"./genesisReplayEvolutionV1.js\";\nimport { handleStripeCheckoutCreate, handleStripeMembershipWebhook } from \"./stripeWebhookMembership.js\";\nimport { buildRealityHealthPayload } from \"./edgeRealityConsistencyV1.js\";\nimport {\n assertWalPeerSocketAuthorityV0,\n buildGatewaySubstrateAuthoritySnapshotV0,\n logGatewaySubstrateAuthorityGuardsV0,\n validateWalPeerFeedSignatureV0\n} from \"./gatewaySubstrateAuthorityV0.js\";\nimport {\n buildSubstrateOperationalSnapshotV0,\n recordWalPeerRejectV0,\n renderSubstratePrometheusMetricsV0\n} from \"./infra/substrateOperationalMetrics.js\";\nimport { ingestClientSubstrateHealthV0 } from \"./rhizohSubstrateHealthIngestV0.js\";\nimport { recordSubstrateOtelEventV0 } from \"./infra/opentelemetrySubstrateV0.js\";\nimport {\n GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n getGenesisWorldObservationIngressStatusV0,\n ingestGenesisWorldObservationV0\n} from \"./genesisContinuityClientIngressV0.js\";\nimport {\n getGenesisContinuitySeqAuditV0,\n noteGenesisContinuitySeqAcceptedV0\n} from \"./genesisContinuitySeqGapV0.js\";\n\n/** Render / Fly / Railway: `PORT` — yoksa `CASTLE_GATEWAY_PORT` — yoksa 8090. */\nconst PORT =\n Number(process.env.PORT) ||\n Number(process.env.CASTLE_GATEWAY_PORT) ||\n 8090;\n/** Spiral / world WebSocket server — set after `wss` init; used by genesis runtime surface. */\nlet spiralWssForGenesis = null;\nconst MAX_MESSAGE_BYTES = Number(process.env.CASTLE_MAX_MESSAGE_BYTES || 32 * 1024);\nconst REQUIRED_GATEWAY_TOKEN = process.env.CASTLE_GATEWAY_TOKEN || \"\";\nconst ALLOWED_ORIGINS = parseAllowedOriginsFromEnv().fromList;\nconst httpCors = createHttpCorsPolicy();\n\n/** @returns {string|null} `Access-Control-Allow-Origin`; null = browser CORS fail. */\nfunction accessControlAllowOriginValue(req) {\n return httpCors.accessControlAllowOriginValue(req);\n}\n\nfunction applyHttpCorsHeaders(req, res) {\n return httpCors.applyHttpCorsHeaders(req, res);\n}\nconst REQUIRE_AUTH = process.env.CASTLE_REQUIRE_AUTH === \"true\";\nconst ALLOW_DEV_ANON = process.env.CASTLE_ALLOW_DEV_ANON !== \"false\";\nconst ALLOW_DEV_HTTP_UID = process.env.CASTLE_ALLOW_DEV_HTTP_UID !== \"false\";\n/** Presence mesh: allow `X-Castle-Guest-Id` when Firebase auth absent (default on). Set `false` to require auth only. */\nconst ALLOW_MESH_GUEST = process.env.CASTLE_ALLOW_MESH_GUEST !== \"false\";\nconst SOCIAL_RETRY_MAX = Math.max(0, Math.min(5, Number(process.env.CASTLE_SOCIAL_RETRY_MAX || 2)));\nconst SOCIAL_RETRY_BASE_MS = Math.max(100, Number(process.env.CASTLE_SOCIAL_RETRY_BASE_MS || 700));\nconst TELEGRAM_BOT_TOKEN = process.env.CASTLE_TELEGRAM_BOT_TOKEN || \"\";\nconst WHATSAPP_TOKEN = process.env.CASTLE_WHATSAPP_TOKEN || \"\";\nconst WHATSAPP_PHONE_NUMBER_ID = process.env.CASTLE_WHATSAPP_PHONE_NUMBER_ID || \"\";\nconst RL_RHIZOH_LLM_PER_MIN = Math.max(5, Number(process.env.CASTLE_RL_RHIZOH_LLM_PER_MIN || 40));\nconst RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN || 30)\n);\nconst RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN || 120));\nconst RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN = Math.max(\n 10,\n Number(process.env.CASTLE_RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN || 60)\n);\nconst RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN = Math.max(8, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN || 48));\nconst RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN = Math.max(4, Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN || 24));\nconst RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN || 18)\n);\nconst RL_EPISTEMIC_SEAL_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_EPISTEMIC_SEAL_PER_MIN || 120));\nconst RL_EPISTEMIC_LOG_PER_MIN = Math.max(20, Number(process.env.CASTLE_RL_EPISTEMIC_LOG_PER_MIN || 240));\nconst EPISTEMIC_SEAL_SECRET = String(\n process.env.CASTLE_EPISTEMIC_SEAL_SECRET || process.env.CASTLE_GATEWAY_TOKEN || \"\"\n).trim();\nconst RL_LLM_CONN_TEST_PER_MIN = Math.max(2, Number(process.env.CASTLE_RL_LLM_CONNECTION_TEST_PER_MIN || 8));\n\n/** One-shot production warnings: CORS surface, auth toggle, dev bypass flags (observatory deploy hygiene). */\nfunction logProductionObservatorySurfaceGuardsV0() {\n if (String(process.env.NODE_ENV || \"\").toLowerCase() !== \"production\") return;\n const tag = \"[GATEWAY_PROD_GUARD]\";\n const primaryCors = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n const corsListEmpty = ALLOWED_ORIGINS.length === 0;\n if (corsListEmpty && !primaryCors) {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty and CASTLE_HTTP_CORS_ORIGIN empty — production still merges RHIZOH_BROWSER_ORIGINS_V1 defaults; set env for preview/staging hosts`\n );\n } else if (corsListEmpty && primaryCors && primaryCors !== \"*\") {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty — only CASTLE_HTTP_CORS_ORIGIN set; add web.app + firebaseapp.com (and previews) to CASTLE_ALLOWED_ORIGINS if clients use alternate hostnames`\n );\n }\n if (primaryCors && /localhost|127\\.0\\.0\\.1/i.test(primaryCors)) {\n console.warn(`${tag} CORS: CASTLE_HTTP_CORS_ORIGIN is localhost while NODE_ENV=production`);\n }\n if (REQUIRE_AUTH) {\n console.warn(\n `${tag} AUTH: CASTLE_REQUIRE_AUTH=true — clients must send a valid gateway credential (otherwise silent 401 on replay/SSE); EventSource cannot set custom headers unless proxied`\n );\n if (REQUIRED_GATEWAY_TOKEN.length < 16) {\n console.warn(`${tag} AUTH: CASTLE_GATEWAY_TOKEN missing or short while require-auth enabled`);\n }\n }\n if (ALLOW_DEV_ANON) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_ANON is not \"false\" — dev anonymous path may be live in production (set CASTLE_ALLOW_DEV_ANON=false)`\n );\n }\n if (ALLOW_DEV_HTTP_UID) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_HTTP_UID is not \"false\" — X-Castle-Dev-Uid bypass risk (set CASTLE_ALLOW_DEV_HTTP_UID=false)`\n );\n }\n}\n\nconst rhizohRuntime = initRhizoh();\n\n/** İstemci: llmKeySource veya keySource — env | user_connection | auto */\nfunction normalizeClientLlmKeySource(raw) {\n const s = String(raw ?? \"\").trim().toLowerCase();\n if (s === \"env\" || s === \"server\") return \"env\";\n if (s === \"user_connection\" || s === \"connection\" || s === \"user\") return \"user_connection\";\n if (s === \"auto\" || s === \"\") return \"auto\";\n return null;\n}\n\n/**\n * Server-side provider wins for env/auto unless user_connection — client bundle may still send provider=openai.\n * @param {\"env\"|\"user_connection\"|\"auto\"} keyMode\n * @param {{ provider?: string, model?: string, apiKey?: string } | null} conn\n * @param {{ provider?: string, model?: string }} payload\n */\nfunction resolveServerLlmProviderV0(keyMode, conn, payload) {\n const envProvider = String(process.env.CASTLE_LLM_PROVIDER || \"\").trim() || undefined;\n const envModel = String(process.env.CASTLE_LLM_MODEL || \"\").trim() || undefined;\n\n if (keyMode === \"user_connection\") {\n return {\n resolvedProvider: conn?.provider || payload?.provider,\n resolvedModel: conn?.model || payload?.model,\n connApiKey: conn?.apiKey || \"\"\n };\n }\n\n if (keyMode === \"env\") {\n return {\n resolvedProvider: envProvider ?? payload?.provider,\n resolvedModel: envModel ?? payload?.model,\n connApiKey: \"\"\n };\n }\n\n if (conn?.provider && conn?.apiKey) {\n return {\n resolvedProvider: conn.provider,\n resolvedModel: conn.model,\n connApiKey: conn.apiKey\n };\n }\n\n return {\n resolvedProvider: envProvider ?? payload?.provider,\n resolvedModel: envModel ?? payload?.model,\n connApiKey: \"\"\n };\n}\n\nfunction jsonReplacerSafeV0(_key, value) {\n if (typeof value === \"bigint\") return Number(value);\n return value;\n}\n\nfunction sendJson(res, statusCode, payload) {\n res.writeHead(statusCode, { \"Content-Type\": \"application/json; charset=utf-8\" });\n try {\n res.end(JSON.stringify(payload, jsonReplacerSafeV0));\n } catch (error) {\n res.end(\n JSON.stringify({\n ok: false,\n error: \"response_serialize_failed\",\n detail: String(error?.message || error)\n })\n );\n }\n}\n\nfunction logRhizohHealth(stage, detail = {}) {\n try {\n console.info(`[CASTLE_${String(stage || \"unknown\")}]`, detail && typeof detail === \"object\" ? detail : {});\n } catch {\n /* noop */\n }\n}\n\nfunction renderInfraMetricsProm() {\n const lines = [\n `castle_gateway_events_processed_total ${infraMetrics.eventsProcessed}`,\n `castle_gateway_duplicate_reject_total ${infraMetrics.duplicateRejects}`,\n `castle_gateway_enqueue_latency_ms ${infraMetrics.enqueueLatencyMs}`,\n `castle_gateway_queue_depth ${infraMetrics.queueDepth}`,\n `castle_gateway_queue_lag_seconds ${(Number(infraMetrics.queueLag || 0) / 1000).toFixed(3)}`,\n `castle_gateway_errors_total ${infraMetrics.errors}`\n ];\n const buckets = infraMetrics.enqueueLatencyBuckets || {};\n for (const k of Object.keys(buckets)) {\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"${k}\"} ${buckets[k]}`);\n }\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"+Inf\"} ${infraMetrics.eventsProcessed}`);\n if (process.env.CASTLE_SUBSTRATE_METRICS !== \"0\") {\n lines.push(renderSubstratePrometheusMetricsV0().trimEnd());\n }\n return lines.join(\"\\n\");\n}\n\nasync function fetchJsonWithTimeout(url, timeoutMs = 1200) {\n const ctl = new AbortController();\n const tid = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const res = await fetch(url, { signal: ctl.signal });\n if (!res.ok) return null;\n return await res.json();\n } catch {\n return null;\n } finally {\n clearTimeout(tid);\n }\n}\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction getHttpPathname(req) {\n let p = String(req.url || \"\").split(\"?\")[0];\n /* Reverse proxies / clients sometimes append `/` — avoid silent 404 on e.g. `/rhizoh/genesis/stream/`. */\n while (p.length > 1 && p.endsWith(\"/\")) {\n p = p.slice(0, -1);\n }\n return p;\n}\n\nfunction rhizohLlmEnvConfigured() {\n const p = String(process.env.CASTLE_LLM_PROVIDER || \"openai\").toLowerCase();\n const key =\n p === \"anthropic\"\n ? process.env.ANTHROPIC_API_KEY\n : p === \"gemini\"\n ? process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY\n : p === \"xai\"\n ? process.env.XAI_API_KEY\n : p === \"deepseek\"\n ? process.env.DEEPSEEK_API_KEY\n : p === \"mistral\"\n ? process.env.MISTRAL_API_KEY\n : p === \"openrouter\"\n ? process.env.OPENROUTER_API_KEY\n : process.env.OPENAI_API_KEY;\n return !!String(key || \"\").trim();\n}\n\n/**\n * @param {import(\"firebase-admin/firestore\").Firestore | null} db\n * @param {number} timeoutMs\n */\nasync function probeFirestoreReachable(db, timeoutMs = 2000) {\n if (!db) return { ok: false, reason: \"no_db\" };\n const timeout = sleep(timeoutMs).then(() => ({ ok: false, reason: \"timeout\" }));\n const work = db\n .collection(\"_castle_gateway_health\")\n .doc(\"ping\")\n .get()\n .then(() => ({ ok: true, reason: \"reachable\" }))\n .catch((e) => ({ ok: false, reason: String(e?.message || e || \"firestore_error\") }));\n return Promise.race([work, timeout]);\n}\n\nfunction stripHtml(text = \"\") {\n return String(text)\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \" \")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \" \")\n .replace(/<[^>]+>/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction haversineMeters(aLat, aLon, bLat, bLon) {\n const R = 6371000;\n const dLat = ((bLat - aLat) * Math.PI) / 180;\n const dLon = ((bLon - aLon) * Math.PI) / 180;\n const sa = Math.sin(dLat / 2);\n const sb = Math.sin(dLon / 2);\n const x = sa * sa + Math.cos((aLat * Math.PI) / 180) * Math.cos((bLat * Math.PI) / 180) * sb * sb;\n return 2 * R * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));\n}\n\nasync function checkPublishTargetHealth(protocol, target) {\n const p = String(protocol || \"\").toUpperCase();\n const t = String(target || \"\").trim();\n if (!t) return { ok: false, reason: \"target_missing\", shouldReconnect: false };\n if (p === \"RTMP\") {\n const ok = t.startsWith(\"rtmp://\") || t.startsWith(\"rtmps://\");\n return { ok, reason: ok ? \"rtmp_target_format_ok\" : \"invalid_rtmp_target\", shouldReconnect: !ok, reconnectInSec: 15 };\n }\n if (p === \"WHIP\" || p === \"SFU\") {\n try {\n const res = await fetch(t, { method: \"OPTIONS\" });\n const ok = res.status < 500;\n return { ok, reason: `http_${res.status}`, shouldReconnect: !ok, reconnectInSec: 8 };\n } catch {\n return { ok: false, reason: \"network_unreachable\", shouldReconnect: true, reconnectInSec: 8 };\n }\n }\n return { ok: true, reason: \"unchecked_protocol\", shouldReconnect: false };\n}\n\nasync function fetchTextFromUrl(url) {\n const res = await fetch(url, { method: \"GET\" });\n if (!res.ok) throw new Error(`fetch_${res.status}`);\n const ct = String(res.headers.get(\"content-type\") || \"\");\n if (ct.includes(\"application/pdf\")) throw new Error(\"pdf_binary_not_supported_directly\");\n const body = await res.text();\n return stripHtml(body).slice(0, 14000);\n}\n\nfunction buildTaskActionGraph(task = \"\", context = {}) {\n const t = String(task || \"\").toLowerCase();\n if (t.includes(\"dinner\") || t.includes(\"akşam yemeği\") || t.includes(\"aksam yemegi\")) {\n return {\n taskTitle: \"Dinner Prep Planner\",\n nodes: [\n { id: \"n1\", title: \"Kitchen preheat\", action: \"smart-oven.preheat\", params: { temperatureC: 190, mode: \"fan\" } },\n { id: \"n2\", title: \"Lighting comfort\", action: \"smart-light.set_scene\", params: { scene: \"warm_dinner\" } },\n { id: \"n3\", title: \"Fridge inventory check\", action: \"smart-fridge.inventory_scan\", params: {} },\n { id: \"n4\", title: \"Reminder notification\", action: \"smart-watch.notify\", params: { text: \"Dinner prep started.\" } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" },\n { from: \"n3\", to: \"n4\" }\n ]\n };\n }\n return {\n taskTitle: task || \"General Planner\",\n nodes: [\n { id: \"n1\", title: \"Safety check\", action: \"device.safety_check\", params: { context } },\n { id: \"n2\", title: \"Primary action\", action: \"device.execute_task\", params: { task } },\n { id: \"n3\", title: \"Report\", action: \"studio.timeline_note\", params: { task } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" }\n ]\n };\n}\n\nfunction buildEthicsPrograms() {\n return {\n policyVersion: \"v1\",\n tracks: [\n {\n id: \"elderly-care-assist\",\n audience: \"elderly\",\n title: \"Elderly Assistance Track\",\n principles: [\"human-in-the-loop\", \"consent-first\", \"low-friction alerts\"],\n modules: [\"medication-reminder\", \"fall-risk-check\", \"family-notify\", \"daily-companion-brief\"]\n },\n {\n id: \"kids-safe-learning\",\n audience: \"children\",\n title: \"Kids Safe Learning Track\",\n principles: [\"guardian-consent\", \"age-appropriate-language\", \"content-filtering\"],\n modules: [\"study-coach\", \"screen-time-balance\", \"safe-social-share\", \"emergency-contact\"]\n }\n ]\n };\n}\n\nasync function deliverViaTelegram(channel, text) {\n const chatId = String(channel.endpoint || \"\").trim();\n if (!TELEGRAM_BOT_TOKEN) throw new Error(\"telegram_token_missing\");\n if (!chatId) throw new Error(\"telegram_chat_id_missing\");\n const url = `https://api.telegram.org/bot${encodeURIComponent(TELEGRAM_BOT_TOKEN)}/sendMessage`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ chat_id: chatId, text })\n });\n if (!res.ok) throw new Error(`telegram_http_${res.status}`);\n return { provider: \"telegram\", status: res.status };\n}\n\nasync function deliverViaWhatsapp(channel, text) {\n const to = String(channel.endpoint || \"\").trim();\n if (!WHATSAPP_TOKEN) throw new Error(\"whatsapp_token_missing\");\n if (!WHATSAPP_PHONE_NUMBER_ID) throw new Error(\"whatsapp_phone_number_id_missing\");\n if (!to) throw new Error(\"whatsapp_to_missing\");\n const url = `https://graph.facebook.com/v22.0/${encodeURIComponent(WHATSAPP_PHONE_NUMBER_ID)}/messages`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${WHATSAPP_TOKEN}` },\n body: JSON.stringify({ messaging_product: \"whatsapp\", to, type: \"text\", text: { body: text } })\n });\n if (!res.ok) throw new Error(`whatsapp_http_${res.status}`);\n return { provider: \"whatsapp\", status: res.status };\n}\n\nasync function deliverViaWebhook(channel, text) {\n const endpoint = String(channel.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"webhook_endpoint_missing\");\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text, platform: channel.platform || \"webhook\", ts: Date.now() })\n });\n if (!res.ok) throw new Error(`webhook_http_${res.status}`);\n return { provider: \"webhook\", status: res.status };\n}\n\nasync function deliverSocialMessage(channel, text) {\n const platform = String(channel.platform || \"webhook\").toLowerCase();\n if (platform === \"telegram\") return deliverViaTelegram(channel, text);\n if (platform === \"whatsapp\") return deliverViaWhatsapp(channel, text);\n return deliverViaWebhook(channel, text);\n}\n\nasync function deliverSocialWithRetry(channel, text) {\n let attempt = 0;\n let lastError = null;\n while (attempt <= SOCIAL_RETRY_MAX) {\n try {\n const result = await deliverSocialMessage(channel, text);\n return { ok: true, attempt: attempt + 1, ...result };\n } catch (error) {\n lastError = error;\n if (attempt >= SOCIAL_RETRY_MAX) break;\n const jitter = Math.floor(Math.random() * 90);\n await sleep(SOCIAL_RETRY_BASE_MS * Math.pow(2, attempt) + jitter);\n }\n attempt += 1;\n }\n return { ok: false, attempt: attempt + 1, error: String(lastError?.message || \"delivery_failed\") };\n}\n\nasync function dispatchCommandToAdapter(device, command) {\n const adapter = String(command?.adapter || device?.adapter || \"websocket\").toLowerCase();\n const endpoint = String(device?.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"device_endpoint_missing\");\n if (adapter === \"mqtt\") {\n // MQTT delivery is proxied via a bridge endpoint (HTTP webhook style).\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n type: \"mqtt-command\",\n topic: `castle/devices/${device.id}/commands`,\n action: command.action,\n params: command.params || {}\n })\n });\n if (!res.ok) throw new Error(`mqtt_bridge_http_${res.status}`);\n return { ok: true, adapter, via: \"http-bridge\", status: res.status };\n }\n if (!endpoint.startsWith(\"ws://\") && !endpoint.startsWith(\"wss://\")) throw new Error(\"invalid_websocket_endpoint\");\n const sendResult = await new Promise((resolve, reject) => {\n const ws = new WebSocket(endpoint);\n const timer = setTimeout(() => {\n try {\n ws.close();\n } catch {\n /* noop */\n }\n reject(new Error(\"websocket_timeout\"));\n }, 3000);\n ws.on(\"open\", () => {\n ws.send(\n JSON.stringify({\n type: \"castle-command\",\n commandId: command.id,\n deviceId: command.deviceId,\n action: command.action,\n params: command.params || {}\n }),\n (err) => {\n clearTimeout(timer);\n ws.close();\n if (err) reject(new Error(\"websocket_send_failed\"));\n else resolve({ ok: true, adapter: \"websocket\", via: \"ws\" });\n }\n );\n });\n ws.on(\"error\", () => {\n clearTimeout(timer);\n reject(new Error(\"websocket_connect_failed\"));\n });\n });\n return sendResult;\n}\n\nfunction readHttpJson(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => {\n if (!buf) return resolve({});\n const payload = safeJsonParse(buf);\n if (!payload || typeof payload !== \"object\") return reject(new Error(\"invalid_json_payload\"));\n resolve(payload);\n });\n req.on(\"error\", reject);\n });\n}\n\n/** Ham gövde (HMAC doğrulaması için). */\nfunction readHttpBodyText(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => resolve(buf));\n req.on(\"error\", reject);\n });\n}\n\nfunction maskOpaqueId(value) {\n const t = String(value || \"\");\n if (!t) return \"(empty)\";\n if (t.length <= 8) return \"****\";\n return `${t.slice(0, 4)}…${t.slice(-2)}`;\n}\n\nfunction readGatewayHttpToken(req) {\n const x = String(req.headers?.[\"x-castle-gateway-token\"] || \"\").trim();\n if (x) return x;\n const auth = String(req.headers?.authorization || \"\");\n const m = /^Bearer\\s+(.+)$/i.exec(auth);\n return m ? String(m[1] || \"\").trim() : \"\";\n}\n\nasync function resolveHttpUser(req) {\n const authResult = await verifyClientToken(req);\n if (authResult?.ok && authResult.user?.uid) {\n return { ok: true, uid: String(authResult.user.uid), mode: authResult.kind || \"auth\" };\n }\n if (ALLOW_DEV_ANON && ALLOW_DEV_HTTP_UID) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) return { ok: true, uid: `dev-${devUid.slice(0, 64)}`, mode: \"dev-header\" };\n }\n return { ok: false, reason: authResult?.reason || \"auth_required\" };\n}\n\n/**\n * Epistemic ledger ingest — Firebase uid OR valid transport token + X-Castle-Dev-Uid.\n * Prod disables CASTLE_ALLOW_DEV_HTTP_UID; LLM may still work while telemetry 401'd without this.\n */\nasync function resolveEpistemicIngestActor(req) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok && tok === REQUIRED_GATEWAY_TOKEN) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) {\n return { ok: true, uid: `gw-telemetry-${devUid.slice(0, 64)}`, mode: \"gateway-token-dev-uid\" };\n }\n return { ok: false, reason: \"dev_uid_required\" };\n }\n if (tok) {\n return { ok: false, reason: \"gateway_token_mismatch\" };\n }\n }\n const auth = await resolveHttpUser(req);\n if (auth.ok) return auth;\n return { ok: false, reason: auth.reason || \"auth_required\" };\n}\n\n/** Mesh actor: Firebase uid, dev header, or `guest:<uuid>` via header / `guestId` query (SSE cannot set headers). */\nasync function resolveMeshActor(req) {\n const auth = await resolveHttpUser(req);\n if (auth.ok) return { ok: true, clientUid: auth.uid, mode: auth.mode };\n if (ALLOW_MESH_GUEST) {\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const gid = String(req.headers?.[\"x-castle-guest-id\"] || q.searchParams.get(\"guestId\") || \"\").trim();\n if (gid) {\n const normalized = gid.startsWith(\"guest:\") ? gid.slice(0, 180) : `guest:${gid.slice(0, 160)}`;\n return { ok: true, clientUid: normalized, mode: \"guest-header\" };\n }\n }\n return { ok: false, reason: \"mesh_auth_required\" };\n}\n\n/** Live genesis runtime JSON (GET /rhizoh/genesis/runtime + SSE infra sampler). */\nasync function buildGenesisRuntimeSurfacePayloadLive() {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n let spiralWsActive = 0;\n if (spiralWssForGenesis) {\n for (const c of spiralWssForGenesis.clients) {\n if (c.readyState === 1) spiralWsActive += 1;\n }\n }\n const base = buildGenesisRuntimeSurfacePayload({\n infraMetrics,\n scoredHealth: scored,\n rhizohEnterpriseMetrics,\n mesh: meshContinuityAggregate(),\n workerHealth: worker,\n port: PORT,\n spiralWebSocketClientsActive: spiralWsActive\n });\n return {\n ...base,\n replayFingerprint: computeGenesisReplayFingerprintV0(base),\n gatewayCapabilities: buildGenesisGatewayCapabilitiesV0(),\n genesisStream: {\n schema: \"castle.genesis.stream_cursor.v0\",\n lastAcceptedSeq: getGenesisContinuitySeq(),\n clientIngress: {\n schema: GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n contract: \"castle.world_observation.ingress_envelope.v1\",\n postPath: rhizohRuntime.routes.genesisIngress,\n closure: getGenesisWorldObservationIngressStatusV0()\n },\n seqContinuity: getGenesisContinuitySeqAuditV0(),\n eventArchive: genesisContinuityEventArchiveEnabled()\n ? {\n enabled: true,\n projection: GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION,\n queryPath: rhizohRuntime.routes.genesisContinuityEvents\n }\n : { enabled: false },\n replayRouter: {\n schema: GENESIS_REPLAY_ROUTER_SCHEMA,\n queryPath: rhizohRuntime.routes.genesisReplay,\n queryHint: \"from=&to= or range=from-to; optional type=&checkpoints=0\",\n temporalDiffPath: rhizohRuntime.routes.genesisReplayDiff,\n temporalDiffSchema: GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA,\n equivalencePath: rhizohRuntime.routes.genesisReplayEquivalence,\n analyticsPath: rhizohRuntime.routes.genesisReplayAnalytics,\n evolutionPath: rhizohRuntime.routes.genesisReplayEvolution\n }\n }\n };\n}\n\nconst httpServer = createServer(async (req, res) => {\n const corsAllow = applyHttpCorsHeaders(req, res);\n if (req.method === \"OPTIONS\") {\n if (!corsAllow && req.headers?.origin) {\n console.warn(\"[GATEWAY_CORS] preflight rejected for origin:\", req.headers.origin);\n }\n res.writeHead(corsAllow ? 204 : 403);\n res.end();\n return;\n }\n\n const pathname = getHttpPathname(req);\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/genesis/__ping\") {\n sendJson(res, 200, {\n ok: true,\n pid: process.pid,\n entry: __filename,\n time: Date.now(),\n genesis: true\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/epistemic/__probe\") {\n sendJson(res, 200, {\n ok: true,\n epistemicIngestActorV2: true,\n routes: {\n seal: rhizohRuntime.routes.epistemicSeal,\n logsBatch: rhizohRuntime.routes.epistemicLogsBatch\n },\n hasGatewayToken: Boolean(REQUIRED_GATEWAY_TOKEN),\n gatewayTokenLen: REQUIRED_GATEWAY_TOKEN ? REQUIRED_GATEWAY_TOKEN.length : 0\n });\n return;\n }\n\n if (\n req.method === \"GET\" &&\n (pathname === \"/rhizoh/live/world-feed\" ||\n pathname === \"/rhizoh/live/sports-bundle\" ||\n pathname === \"/rhizoh/live/news-headlines\")\n ) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_live_world_feed:${ip}`, RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const out =\n pathname === \"/rhizoh/live/sports-bundle\"\n ? await handleLiveSportsBundleGetV0()\n : pathname === \"/rhizoh/live/news-headlines\"\n ? await handleLiveNewsHeadlinesGetV0(req)\n : await handleLiveWorldFeedGetV0(req);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/webhooks/stripe\") {\n await handleStripeMembershipWebhook(req, res);\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/billing/stripe/checkout\") {\n await handleStripeCheckoutCreate(req, res, { readHttpJson, resolveHttpUser, sendJson });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/live\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, live: false, dns: true, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n sendJson(res, 200, { ok: true, live: true, dns: true, service: \"castle-gateway\", ts: Date.now() });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/cors\") {\n const reqOrigin = normalizeHttpOrigin(req.headers?.origin);\n const allowOrigin = accessControlAllowOriginValue(req);\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n requestOrigin: reqOrigin || null,\n allowed: Boolean(allowOrigin),\n allowOrigin: allowOrigin || null,\n allowSetSize: httpCors.allowSet.size\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/ready\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, ready: false, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n try {\n const persistence = getFirebasePersistence();\n const llmProbe = rhizohLlmEnvConfigured();\n sendJson(res, 200, {\n ok: true,\n ready: true,\n dns: true,\n persistence: persistence.mode,\n service: \"castle-gateway\",\n gateway: \"ok\",\n llm: llmProbe ? \"ok\" : \"degraded\",\n spine: llmProbe ? \"ok\" : \"partial\"\n });\n } catch (e) {\n sendJson(res, 503, { ok: false, ready: false, reason: String(e?.message || e), service: \"castle-gateway\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/reality\") {\n const payload = buildRealityHealthPayload();\n sendJson(res, payload.ok ? 200 : 503, payload);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/substrate\") {\n const authority = buildGatewaySubstrateAuthoritySnapshotV0();\n const operational = buildSubstrateOperationalSnapshotV0();\n const drift = buildRealityHealthPayload();\n const ok = Boolean(authority.productionAuthoritySatisfied) && drift.ok;\n sendJson(res, ok ? 200 : 503, {\n ok,\n service: \"castle-gateway\",\n ts: Date.now(),\n authority,\n operational,\n drift: { ok: drift.ok, errors: drift.drift?.errors, warnings: drift.drift?.warnings }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/substrate/health\") {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const body = await readHttpJson(req);\n const uid = (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestClientSubstrateHealthV0(body, { uid, clientId: uid });\n if (!result.ok) {\n sendJson(res, 400, result);\n return;\n }\n recordSubstrateOtelEventV0(\"client_health_ingest\", { uid: String(uid).slice(0, 32) });\n sendJson(res, 200, result);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/deps\") {\n const t0 = Date.now();\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, {\n ok: false,\n dns: true,\n llm: false,\n firestore: false,\n phase: \"maintenance\",\n latencyMs: Date.now() - t0,\n service: \"castle-gateway\"\n });\n return;\n }\n const persistence = getFirebasePersistence();\n let firestoreOk = persistence.mode !== \"firebase\";\n let firestoreDetail = persistence.mode === \"file\" ? \"file_mode\" : \"not_applicable\";\n if (persistence.mode === \"firebase\" && persistence.db) {\n const pr = await probeFirestoreReachable(persistence.db, 2000);\n firestoreOk = pr.ok;\n firestoreDetail = pr.reason || (pr.ok ? \"reachable\" : \"fail\");\n } else if (persistence.mode === \"firebase\" && !persistence.db) {\n firestoreOk = false;\n firestoreDetail = \"firebase_not_initialized\";\n }\n const llmOk = rhizohLlmEnvConfigured();\n const overallOk = llmOk && firestoreOk;\n const latencyMs = Date.now() - t0;\n sendJson(res, 200, {\n ok: overallOk,\n dns: true,\n llm: llmOk,\n firestore: persistence.mode === \"firebase\" ? firestoreOk : false,\n persistence: persistence.mode,\n firestoreDetail,\n latencyMs,\n service: \"castle-gateway\",\n wsPort: PORT\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health\") {\n const persistence = getFirebasePersistence();\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n wsPort: PORT,\n persistence: persistence.mode,\n presenceMesh: \"sse\"\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/health\") {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n sendJson(res, 200, {\n ok: true,\n role: \"gateway\",\n queueLagMs: infraMetrics.queueLag,\n queueDepth: infraMetrics.queueDepth,\n errors: infraMetrics.errors,\n status: scored.status,\n score: scored.score,\n reasons: scored.reasons,\n worker: worker\n ? {\n status: worker.status,\n score: worker.score,\n reasons: worker.reasons\n }\n : null,\n timestamp: Date.now()\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisRuntime) {\n const payload = await buildGenesisRuntimeSurfacePayloadLive();\n sendJson(res, 200, payload);\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.genesisIngress) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const actor = await resolveMeshActor(req);\n const clientId = actor.ok ? actor.clientUid : (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestGenesisWorldObservationV0(body, { clientId });\n if (!result.ok) {\n const status =\n result.error === \"rate_limited\" || result.error === \"agent_spoke_throttled\"\n ? 429\n : result.error === \"payload_too_large\"\n ? 413\n : 400;\n sendJson(res, status, result);\n return;\n }\n sendJson(res, result.idempotent ? 200 : 202, result);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisStream) {\n const corsOrigin = accessControlAllowOriginValue(req);\n if (!corsOrigin && String(process.env.NODE_ENV || \"\").toLowerCase() === \"production\") {\n const og = String(req.headers?.origin || \"\").trim();\n if (og) {\n console.warn(\"[GATEWAY] genesis SSE: missing Access-Control-Allow-Origin for browser Origin (CORS whitelist mismatch):\", og);\n }\n }\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n res.write(\"retry: 3000\\n\\n\");\n res.write(\": genesis continuity stream\\n\\n\");\n res.write(\"event: genesis\\n\");\n res.write(`data: {\"booted\":true,\"pid\":${process.pid}}\\n\\n`);\n registerGenesisSseClient(res, req);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLatest) {\n const cp = getLatestGenesisSignedCheckpoint();\n if (!cp) {\n sendJson(res, 404, { ok: false, error: \"no_checkpoint_yet\", hint: \"wait_for_seq_interval_or_configure_signing_secret\" });\n return;\n }\n sendJson(res, 200, { ok: true, checkpoint: cp });\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(rhizohRuntime.routes.genesisCheckpointBySeqPrefix)) {\n const tail = pathname.slice(rhizohRuntime.routes.genesisCheckpointBySeqPrefix.length).split(\"/\")[0];\n const seq = parseInt(String(tail || \"\").trim(), 10);\n const out = await genesisCheckpointQueryBySeqV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"checkpoint_not_found\") {\n sendJson(res, 404, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointRange) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryRangeV0(from, to);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"range_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLineage) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const seq = parseInt(String(u.searchParams.get(\"seq\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryLineageV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"lineage_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisContinuityEvents) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const limit = parseInt(String(u.searchParams.get(\"limit\") || \"\").trim(), 10);\n const out = await queryGenesisContinuityEventArchiveV0(from, to, type, limit);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\" || out.error === \"event_archive_disabled\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"archive_file_too_large\") {\n sendJson(res, 413, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplay) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await resolveGenesisReplayRouterV1({\n from: fromSeq,\n to: toSeq,\n type,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayDiff) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const out = await computeGenesisReplayTemporalDiffV1({ from: fromSeq, to: toSeq, type });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEquivalence) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from1 = parseInt(String(u.searchParams.get(\"from1\") || \"\").trim(), 10);\n const to1 = parseInt(String(u.searchParams.get(\"to1\") || \"\").trim(), 10);\n const from2 = parseInt(String(u.searchParams.get(\"from2\") || \"\").trim(), 10);\n const to2 = parseInt(String(u.searchParams.get(\"to2\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await compareGenesisReplayEquivalenceV1({\n from1,\n to1,\n from2,\n to2,\n type,\n includeCheckpoints\n });\n if (\n !out.ok &&\n (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")\n ) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayAnalytics) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayAnalyticsV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEvolution) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const collapseWindows = parseInt(String(u.searchParams.get(\"collapseWindows\") || \"0\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayEvolutionV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n collapseWindows,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/metrics\") {\n res.writeHead(200, { \"Content-Type\": \"text/plain; version=0.0.4; charset=utf-8\" });\n res.end(renderInfraMetricsProm());\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.infraPrometheusMetrics) {\n if (process.env.CASTLE_PROMETHEUS_METRICS !== \"1\") {\n sendJson(res, 404, { ok: false, error: \"prometheus_metrics_disabled\" });\n return;\n }\n res.writeHead(200, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(renderRhizohPrometheusMetrics());\n return;\n }\n if (req.method === \"GET\" && pathname.startsWith(\"/infra/traces/\")) {\n const traceId = decodeURIComponent(pathname.slice(\"/infra/traces/\".length));\n const item = await getTraceById(traceId);\n if (!item) return sendJson(res, 404, { ok: false, error: \"trace_not_found\" });\n return sendJson(res, 200, { ok: true, trace: item });\n }\n if (req.method === \"GET\" && pathname === \"/infra/traces\") {\n const u = new URL(String(req.url || \"/infra/traces\"), `http://${req.headers.host || \"localhost\"}`);\n const sessionId = String(u.searchParams.get(\"sessionId\") || \"\").trim();\n const items = sessionId ? await listTracesBySession(sessionId) : [];\n return sendJson(res, 200, { ok: true, sessionId, traces: items });\n }\n\n /** Presence room mesh (C): JOIN / LEAVE / SNAPSHOT / DELTA / REPLAY (HTTP); SUBSCRIBE = SSE stream. */\n if (req.method === \"POST\" && pathname === \"/presence/mesh/join\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid, clientUid: actor.clientUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/leave\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshLeave(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/snapshot\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n sendJson(res, 200, meshSnapshot(roomUid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/delta\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, MAX_MESSAGE_BYTES);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const seq = meshAppendDelta(roomUid, actor.clientUid, {\n node: body?.node,\n projectionPatch: body?.projectionPatch,\n writerSubject: body?.writerSubject\n });\n sendJson(res, 200, { ok: true, roomUid, seq });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/replay\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const fromNodeId = body?.fromNodeId != null ? String(body.fromNodeId) : \"\";\n const fromSeq = body?.fromSeq;\n if (fromNodeId) {\n sendJson(res, 200, meshReplayFromNodeId(roomUid, fromNodeId));\n return;\n }\n sendJson(res, 200, meshReplayFromSeq(roomUid, fromSeq));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/presence/mesh/subscribe\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const roomUid = String(q.searchParams.get(\"roomUid\") || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n const write = (chunk) => {\n try {\n res.write(chunk);\n } catch {\n /* closed */\n }\n };\n const unsub = meshSubscribeSse(roomUid, write);\n const onClose = () => {\n unsub();\n meshLeave(roomUid, actor.clientUid);\n try {\n res.end();\n } catch {\n /* ignore */\n }\n };\n req.on(\"close\", onClose);\n req.on(\"aborted\", onClose);\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, uid: auth.uid, mode: auth.mode, items: await listConnections(auth.uid) });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const id = await createConnection(auth.uid, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"create_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n await updateConnection(auth.uid, id, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"update_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"DELETE\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n await deleteConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"delete_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections/test\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n if (!checkHttpRateLimit(`llm_conn_test:${auth.uid}`, RL_LLM_CONN_TEST_PER_MIN, 60_000)) {\n return sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n }\n const hasBodyKey = !!String(payload?.apiKey || \"\").trim();\n const result = await queryRhizohLlm(\n {\n message: String(payload?.message || \"Ping from Castle Gateway\"),\n context: payload?.context || { source: \"connection_test\" },\n provider: payload?.provider,\n model: payload?.model,\n apiKey: payload?.apiKey || \"\"\n },\n { allowExternalApiKey: true, llmKeySource: hasBodyKey ? \"auto\" : \"env\" }\n );\n logLlmAccess({\n route: \"/llm/connections/test\",\n uid: auth.uid,\n llmKeyBillingOwner: result.llmKeyBillingOwner,\n llmKeyOrigin: result.llmKeyOrigin,\n provider: result.provider,\n model: result.model,\n connectionId: null\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"connection_test_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAgentIdentities(auth.uid);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerAgentIdentity({\n ownerUid: auth.uid,\n agentId: payload?.agentId,\n role: payload?.role,\n personaSeed: payload?.personaSeed || {},\n capabilityLevel: payload?.capabilityLevel\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/agents/identities/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const owns = await getAgentIdentity(id);\n if (!owns || owns.ownerUid !== auth.uid) return sendJson(res, 403, { ok: false, error: \"forbidden\" });\n const payload = await readHttpJson(req);\n const row = await updateAgentIdentity(id, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/academy/events\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/academy/events\"}`);\n const status = String(url.searchParams.get(\"status\") || \"\");\n const items = await listAcademyEvents({ ownerUid: auth.uid, status, limit: 120 });\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/academy/events\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const ev = await queueAcademyEvent({\n type: payload?.type,\n roomId: payload?.roomId,\n ownerUid: auth.uid,\n participants: payload?.participants || [],\n topic: payload?.topic || \"academy\",\n payload: payload?.payload || {}\n });\n sendJson(res, 200, { ok: true, event: ev });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && String(req.url || \"\").startsWith(\"/academy/events/\") && String(req.url || \"\").endsWith(\"/resolve\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const parts = String(req.url || \"\").split(\"/\").filter(Boolean);\n const eventId = parts[2] || \"\";\n try {\n const payload = await readHttpJson(req);\n const out = await resolveAcademyEvent(eventId, payload || {});\n sendJson(res, 200, { ok: true, event: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_resolve_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/memory\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/memory\"}`);\n const scope = url.searchParams.get(\"scope\") === \"agents\" ? \"agents\" : \"users\";\n const id = String(url.searchParams.get(\"id\") || (scope === \"agents\" ? \"\" : auth.uid));\n const kind = String(url.searchParams.get(\"kind\") || \"\");\n const limit = Number(url.searchParams.get(\"limit\") || 40);\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n sendJson(res, 200, { ok: true, scope, id, kind, items: await listMemories({ scope, id, kind, limit }) });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const profile = await getPersonaGoalMemory(auth.uid);\n sendJson(res, 200, { ok: true, profile });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const profile = await setPersonaGoalMemory({\n uid: auth.uid,\n personaPatch: payload?.personaPatch || {},\n goals: Array.isArray(payload?.goals) ? payload.goals : null,\n preferencesPatch: payload?.preferencesPatch || {}\n });\n sendJson(res, 200, { ok: true, profile });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"profile_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/compact\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const out = await autoCompactMemories({ scope, id });\n sendJson(res, 200, { ok: true, scope, id, ...out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_compact_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/context\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const context = await getMemoryContext({\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n query: String(payload?.query || \"\"),\n limit: Number(payload?.limit || 80)\n });\n sendJson(res, 200, { ok: true, context });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_context_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/place-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const placeName = String(payload?.placeName || \"\").trim();\n if (!placeName) return sendJson(res, 400, { ok: false, error: \"placeName_required\" });\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const persona = String(payload?.waypointPersona || payload?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(placeName)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Place: ${placeName}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Give a concise conversational briefing about this place for a walking-route companion.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-place-brief\", placeName, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `PLACE_BRIEF:${placeName} => ${String(summary.reply || \"\").slice(0, 900)}`,\n tags: [\"place\", \"brief\", \"event-layer\"],\n importance: 0.62,\n meta: { placeName }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"place-brief\",\n text: `${placeName} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: {\n placeName,\n directive: summary.directive,\n lat: hit?.lat ? Number(hit.lat) : null,\n lon: hit?.lon ? Number(hit.lon) : null\n }\n });\n sendJson(res, 200, { ok: true, place: hit || null, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"place_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/route-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const waypoints = Array.isArray(payload?.waypoints) ? payload.waypoints.slice(0, 24) : [];\n const out = [];\n for (const wp of waypoints) {\n const name = String(wp?.name || \"\").trim();\n if (!name) continue;\n const persona = String(wp?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(name)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Waypoint: ${name}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Generate a concise spoken briefing for route waypoint.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-route-brief\", waypoint: name, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n out.push({\n name,\n persona,\n place: hit || null,\n reply: summary.reply,\n directive: summary.directive\n });\n }\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `ROUTE_BRIEF: waypoints=${out.length} ${out.map((x) => `${x.name}:${x.persona}`).join(\" | \")}`.slice(0, 1300),\n tags: [\"route\", \"waypoint\", \"brief\", \"event-layer\"],\n importance: 0.74,\n meta: { count: out.length }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"route-brief\",\n text: out.map((x) => `${x.name}:${String(x.reply || \"\").slice(0, 140)}`).join(\" | \").slice(0, 1400),\n roomId: \"studio-main\",\n meta: {\n waypointCount: out.length,\n waypoints: out.map((x) => ({\n name: x.name,\n persona: x.persona,\n lat: x.place?.lat ? Number(x.place.lat) : null,\n lon: x.place?.lon ? Number(x.place.lon) : null\n }))\n }\n });\n sendJson(res, 200, { ok: true, items: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"route_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/pdf-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const title = String(payload?.title || \"document\");\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const providedText = String(payload?.text || \"\");\n const url = String(payload?.url || \"\");\n let content = providedText;\n if (!content && url) content = await fetchTextFromUrl(url);\n if (!content) return sendJson(res, 400, { ok: false, error: \"text_or_supported_url_required\" });\n const summary = await queryRhizohLlm(\n {\n message: `Summarize this document and provide key points + practical actions.\\nTitle:${title}\\nContent:\\n${content.slice(0, 12000)}`,\n context: { source: \"event-layer-pdf-brief\", title },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `DOC_BRIEF:${title} => ${String(summary.reply || \"\").slice(0, 1000)}`,\n tags: [\"document\", \"brief\", \"library\", \"event-layer\"],\n importance: 0.7,\n meta: { title, url: url || null }\n });\n await addTranscript(auth.uid, {\n source: \"library-brief\",\n eventType: \"pdf-brief\",\n text: `${title} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { title }\n });\n sendJson(res, 200, { ok: true, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/pdf/upload\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req, 8 * 1024 * 1024);\n const job = await enqueuePdfJob({\n uid: auth.uid,\n fileName: payload?.fileName || \"document.pdf\",\n bytesBase64: payload?.contentBase64\n });\n sendJson(res, 200, { ok: true, job });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_upload_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/session\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createPublishSession(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session\",\n text: `Publish session armed: ${row.protocol} ${row.target}`.slice(0, 500),\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: row.id, bridge: row.bridge }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/studio/publish/sessions\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listPublishSessions(auth.uid, 80);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/health-check\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const protocol = String(payload?.protocol || \"WHIP\");\n const target = String(payload?.target || \"\");\n const health = await checkPublishTargetHealth(protocol, target);\n if (payload?.sessionId) {\n await updatePublishSession(auth.uid, String(payload.sessionId), {\n status: health.ok ? \"healthy\" : \"degraded\",\n reconnectPolicy: {\n enabled: true,\n backoffSec: [2, 5, 10, 20],\n nextRetryInSec: health.reconnectInSec || 0,\n maxRetries: 8,\n reason: health.reason\n }\n });\n }\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-health\",\n text: `${protocol} ${target} => ${health.ok ? \"healthy\" : \"degraded\"} (${health.reason})`,\n roomId: \"studio-main\",\n meta: { protocol, target, reconnect: !!health.shouldReconnect }\n });\n sendJson(res, 200, { ok: true, health });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_health_check_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/studio/publish/sessions/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updatePublishSession(auth.uid, id, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session-update\",\n text: `Session ${id} -> ${row.status || \"updated\"}`,\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: id, status: row.status, ingestUrl: row.ingestUrl || \"\" }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/transcripts\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/transcripts\"}`);\n const limit = Math.max(1, Math.min(200, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await listTranscripts(auth.uid, limit);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/transcripts\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await addTranscript(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"transcript_write_failed\" });\n }\n return;\n }\n\n /** E1: broadcast intent → GreenRoom capability → transcript + memory artifact */\n if (req.method === \"POST\" && req.url === \"/studio/capabilities/greenroom\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const traceId = String(payload?.traceId || `TRC-${Date.now().toString(36).toUpperCase()}`);\n const title = String(payload?.title || \"Castle Live Broadcast\").slice(0, 200);\n const intentRaw = String(payload?.intentRaw || \"\").slice(0, 800);\n const audienceEstimate = Math.max(0, Math.min(50_000_000, Number(payload?.audienceEstimate) || 1200));\n const roomId = String(payload?.roomId || \"greenroom-main\");\n const transcriptRow = await addTranscript(auth.uid, {\n source: \"rhizoh\",\n eventType: \"BROADCAST_ROUTED\",\n text: `GreenRoom · ${title}${intentRaw ? `\\nIntent: ${intentRaw}` : \"\"}`,\n roomId,\n meta: {\n ack: \"BROADCAST_ROUTED\",\n capability: \"greenroom\",\n intentType: \"broadcast\",\n traceId,\n audienceEstimate,\n artifact: {\n kind: \"broadcast_card\",\n title,\n traceId,\n roomId,\n createdAt: Date.now()\n }\n }\n });\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n text: `GREENROOM_BROADCAST:${title} trace=${traceId}`,\n tags: [\"broadcast\", \"greenroom\", \"artifact\", \"castle-library\"],\n importance: 0.72,\n kind: \"episodic\",\n meta: { traceId, capability: \"greenroom\", transcriptId: transcriptRow.id }\n });\n sendJson(res, 200, {\n ok: true,\n ack: \"BROADCAST_ROUTED\",\n traceId,\n transcript: transcriptRow,\n replayPath: `/replay/${encodeURIComponent(traceId)}`,\n sharePath: `/replay/${encodeURIComponent(traceId)}`\n });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"greenroom_capability_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/metadata/search\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/metadata/search\"}`);\n const q = String(url.searchParams.get(\"q\") || \"\");\n const limit = Math.max(1, Math.min(300, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await searchMetadata(auth.uid, q, limit);\n sendJson(res, 200, { ok: true, q, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerDevice(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, device: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"device_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const lat = Number(payload?.lat);\n const lon = Number(payload?.lon);\n const speed = Math.max(0, Number(payload?.speed || 0));\n const cfgCenterLat = Number(process.env.CASTLE_GEOFENCE_CENTER_LAT || 41.0082);\n const cfgCenterLon = Number(process.env.CASTLE_GEOFENCE_CENTER_LON || 28.9784);\n const cfgRadiusM = Math.max(100, Number(process.env.CASTLE_GEOFENCE_RADIUS_M || 55000));\n const cfgSpeedCap = Math.max(1, Number(process.env.CASTLE_SPEED_CAP_MPS || 35));\n const dist = Number.isFinite(lat) && Number.isFinite(lon) ? haversineMeters(cfgCenterLat, cfgCenterLon, lat, lon) : 0;\n const geofenceOk = !Number.isFinite(dist) ? true : dist <= cfgRadiusM;\n const speedOk = speed <= cfgSpeedCap;\n const failsafe = !(geofenceOk && speedOk);\n const safety = {\n geofenceOk,\n speedOk,\n failsafe,\n distanceFromCenterM: Number.isFinite(dist) ? dist : null,\n speedCapMps: cfgSpeedCap\n };\n const row = await appendTelemetry(auth.uid, deviceId, {\n lat,\n lon,\n speed,\n battery: payload?.battery,\n mode: payload?.mode || (failsafe ? \"FAILSAFE_HOLD\" : \"ACTIVE\"),\n raw: payload?.raw || {}\n });\n await updateDevice(auth.uid, deviceId, {\n status: failsafe ? \"failsafe\" : \"active\",\n lastTelemetryAt: row.ts,\n safetyState: safety\n });\n if (failsafe) {\n await addTranscript(auth.uid, {\n source: \"robotics-guardrails\",\n eventType: \"failsafe\",\n text: `Device ${deviceId} failsafe triggered geofenceOk=${geofenceOk} speedOk=${speedOk}`,\n roomId: \"studio-main\",\n meta: { deviceId, safety }\n });\n }\n sendJson(res, 200, { ok: true, telemetry: row, safety, command: failsafe ? \"FAILSAFE_HOLD\" : \"CONTINUE\" });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"telemetry_ingest_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listTelemetry(auth.uid, 150);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createAutomation(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"automation-scheduled\",\n text: `Automation scheduled: ${row.title} at ${new Date(row.scheduleAt).toLocaleString()}`,\n roomId: \"studio-main\",\n meta: { automationId: row.id, action: row.action, deviceId: row.deviceId }\n });\n sendJson(res, 200, { ok: true, automation: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"automation_create_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAutomations(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listDevices(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const devs = await listDevices(auth.uid, 500);\n const d = devs.find((x) => x.id === deviceId);\n if (!d) return sendJson(res, 404, { ok: false, error: \"device_not_found\" });\n const action = String(payload?.action || \"\");\n const caps = Array.isArray(d.capabilityProfile) ? d.capabilityProfile : [];\n if (action && caps.length && !caps.includes(action) && !caps.includes(\"*\")) {\n return sendJson(res, 400, { ok: false, error: \"unsupported_action_for_device\", capabilityProfile: caps });\n }\n const cmd = await enqueueCommand(auth.uid, {\n deviceId,\n action: action || \"NOOP\",\n params: payload?.params || {},\n adapter: payload?.adapter || d.adapter || \"websocket\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-command-queue\",\n eventType: \"command-queued\",\n text: `${cmd.deviceId} -> ${cmd.action}`,\n roomId: \"studio-main\",\n meta: { commandId: cmd.id, adapter: cmd.adapter }\n });\n sendJson(res, 200, { ok: true, command: cmd });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listCommands(auth.uid, 200);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/robotics/commands/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updateCommandStatus(auth.uid, id, payload?.status || \"sent\");\n sendJson(res, 200, { ok: true, command: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/rhizoh/bridge\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const result = await runRhizohBrainV2({\n message: String(payload?.message || \"robotics bridge command\"),\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n worldState: payload?.worldState || {},\n deviceState: payload?.deviceState || {},\n intent: \"robotics-mechanics\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-bridge\",\n eventType: \"robotics-command\",\n text: `Bridge -> ${String(result.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { directive: result.directive, agentId: payload?.agentId || \"\" }\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"robotics_bridge_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/planner/graph\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const graph = buildTaskActionGraph(String(payload?.task || \"\"), payload?.context || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"planner-graph\",\n text: `Planner graph built: ${graph.taskTitle} nodes=${graph.nodes.length}`,\n roomId: \"studio-main\",\n meta: { taskTitle: graph.taskTitle, nodeCount: graph.nodes.length }\n });\n sendJson(res, 200, { ok: true, graph });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"planner_graph_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerSocialChannel(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, channel: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_channel_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listSocialChannels(auth.uid, 100);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/broadcast\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const text = String(payload?.text || \"\").slice(0, 1200);\n const channels = await listSocialChannels(auth.uid, 100);\n const enabled = channels.filter((c) => c.status === \"enabled\");\n const deliveries = [];\n for (const c of enabled) {\n const out = await deliverSocialWithRetry(c, text);\n deliveries.push({ id: c.id, platform: c.platform, ...out });\n }\n const okCount = deliveries.filter((d) => d.ok).length;\n await addTranscript(auth.uid, {\n source: \"social-layer\",\n eventType: \"social-broadcast\",\n text: `Broadcast delivered ${okCount}/${enabled.length} channel(s): ${text.slice(0, 240)}`,\n roomId: \"studio-main\",\n meta: { deliveries }\n });\n sendJson(res, 200, { ok: true, sentChannels: okCount, totalChannels: enabled.length, deliveries });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_broadcast_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/ethics/programs\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, ...buildEthicsPrograms() });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands/dispatch\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const all = await listCommands(auth.uid, 200);\n const queued = all.filter((c) => String(c.status || \"queued\") === \"queued\").slice(0, 50);\n const devices = await listDevices(auth.uid, 500);\n const byId = new Map(devices.map((d) => [d.id, d]));\n const results = [];\n for (const cmd of queued) {\n const dev = byId.get(cmd.deviceId);\n if (!dev) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: \"device_not_found\" });\n continue;\n }\n try {\n const dispatched = await dispatchCommandToAdapter(dev, cmd);\n await updateCommandStatus(auth.uid, cmd.id, \"sent\");\n results.push({ id: cmd.id, ok: true, adapter: dispatched.adapter, via: dispatched.via });\n } catch (error) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: String(error?.message || \"dispatch_failed\") });\n }\n }\n await addTranscript(auth.uid, {\n source: \"robotics-command-dispatch\",\n eventType: \"command-dispatch-batch\",\n text: `Dispatch batch size=${results.length} ok=${results.filter((x) => x.ok).length}`,\n roomId: \"studio-main\",\n meta: { results: results.slice(0, 30) }\n });\n sendJson(res, 200, { ok: true, processed: results.length, results });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"dispatch_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/pdf/jobs/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n const job = await getPdfJob(auth.uid, id);\n if (!job) return sendJson(res, 404, { ok: false, error: \"pdf_job_not_found\" });\n sendJson(res, 200, { ok: true, job });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const row = await appendMemory({\n scope,\n id,\n text: payload?.text,\n tags: payload?.tags || [],\n importance: payload?.importance,\n kind: payload?.kind || \"episodic\",\n meta: { uid: auth.uid, ...(payload?.meta || {}) }\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.externalTruth) {\n try {\n if (process.env.RHIZOH_EXTERNAL_TRUTH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_truth:${ip}`, RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n sendJson(res, 200, buildRhizohExternalGroundTruthPayload(req));\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.productOutcome) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const rawBody = await readHttpBodyText(req, 64 * 1024);\n const body = rawBody.trim() ? safeJsonParse(rawBody) : null;\n if (!body || typeof body !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"invalid_json_payload\" });\n return;\n }\n const sigHdr = String(req.headers?.[\"x-rhizoh-outcome-signature\"] || \"\").trim();\n const hmacV = verifyRhizohOutcomeHmac(rawBody, sigHdr);\n if (!hmacV.ok) {\n sendJson(res, 401, { ok: false, error: hmacV.error || \"outcome_hmac_invalid\" });\n return;\n }\n const srcTokHdr = String(req.headers?.[\"x-rhizoh-outcome-source-token\"] || \"\").trim();\n const srcV = verifyRhizohOutcomeSourceToken(body.source, srcTokHdr);\n if (!srcV.ok) {\n sendJson(res, 401, { ok: false, error: srcV.error || \"outcome_source_token_invalid\" });\n return;\n }\n const subjectKey = String(body.subjectKey || \"\").trim().slice(0, 128);\n if (\n subjectKey &&\n !checkHttpRateLimit(\n `rhizoh_product_outcome_subj:${subjectKey}`,\n RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN,\n 60_000\n )\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_subject\" });\n return;\n }\n const result = ingestRhizohProductOutcomeHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n schemaVersion: \"1.1.0\",\n aggregate: result.aggregate,\n integrity: result.integrity\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"outcome_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.productOutcomeAggregate) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome_agg:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const u = new URL(String(req.url || \"/\"), \"http://localhost\");\n const cohortId = u.searchParams.get(\"cohortId\") || \"\";\n const decisionFingerprint = u.searchParams.get(\"decisionFingerprint\") || \"\";\n const limit = u.searchParams.get(\"limit\") || \"50\";\n const rows = listRhizohProductOutcomeAggregates({\n cohortId: cohortId || undefined,\n decisionFingerprint: decisionFingerprint || undefined,\n limit: Number(limit) || 50\n });\n sendJson(res, 200, { ok: true, schemaVersion: \"1.1.0\", rows });\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.externalLossBatch) {\n try {\n if (process.env.RHIZOH_EXTERNAL_LOSS_BATCH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_loss_batch:${ip}`, RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const result = ingestRhizohExternalLossBatchHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, { ok: true, accepted: result.accepted ?? 0 });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"batch_failed\") });\n }\n return;\n }\n\n function moderationAdminOk(req) {\n const expected = String(process.env.CASTLE_MODERATION_ADMIN_KEY || \"\").trim();\n if (!expected) return false;\n const got = String(req.headers[\"x-castle-moderation-key\"] || \"\").trim();\n return got.length > 0 && got === expected;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.opsAbuseReport) {\n try {\n const body = await readHttpJson(req, 32 * 1024);\n const auth = await resolveHttpUser(req);\n const entry = submitAbuseReportV0({\n reporterUid: auth.ok ? auth.uid : null,\n targetUid: body?.targetUid ?? null,\n category: body?.category,\n detail: body?.detail,\n traceId: body?.traceId ?? null\n });\n sendJson(res, 202, { ok: true, reportId: entry.id, status: entry.status });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsModerationQueue) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n queue: listModerationQueueV0(100),\n softBlocked: listSoftBlockedUsersV0()\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-block\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const out = softBlockUserV0(body?.uid, body?.reason);\n sendJson(res, out.ok ? 200 : 400, out);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-unblock\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n sendJson(res, 200, removeSoftBlockV0(body?.uid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsAgentSnapshots) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n const limit = Number(new URL(req.url || \"\", \"http://local\").searchParams.get(\"limit\") || 50);\n sendJson(res, 200, { ok: true, snapshots: listRecentAgentSnapshotsV0(limit) });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.academicObservatoryExport) {\n const auth = await resolveHttpUser(req);\n const out = handleAcademicObservatoryExportGetV0(req, auth);\n sendJson(res, out.status, out.body);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsHardeningStatus) {\n const auth = await resolveHttpUser(req);\n const principal = auth.ok ? `uid:${auth.uid}` : null;\n const gclSnap = await getGlobalCostLedgerSnapshotV0(principal);\n const economicStrategy = evaluateEconomicStrategyL3V0(gclSnap);\n const hardeningTenantId = resolveHardeningTenantIdV0(auth, req);\n const unifiedState = await buildUnifiedStateNarrativeV0({\n tenantId: hardeningTenantId ?? undefined,\n platformScope: !hardeningTenantId,\n principal\n });\n sendJson(res, 200, {\n ok: true,\n schema: \"rhizoh.ops.hardening_status.v0\",\n containment: readAgentContainmentConfigV0(),\n cost: readCostContainmentConfigV0(),\n gcl: {\n backend: await getGclBackendStatusV0(),\n snapshot: gclSnap\n },\n economicStrategyL3: economicStrategy,\n phasedRollout: await getPhasedRolloutStatsV0(),\n tier: readPhasedRolloutTierV0(),\n principalCost: auth.ok ? await getCostContainmentStatsV0(principal) : null,\n stressTaxonomy: verifyStressTaxonomyCoverageV0(),\n stressConflictResolution: verifyStressConflictResolutionV0(),\n resolutionStabilityEnvelope: verifyResolutionStabilityEnvelopeV0(),\n unifiedState: {\n tenantScope: {\n tenantId: unifiedState.tenantScope?.tenantId,\n scope: unifiedState.tenantScope?.scope,\n noGlobalDerivedState: unifiedState.tenantScope?.noGlobalDerivedState\n },\n narrativeFingerprint: unifiedState.narrativeFingerprint,\n screenshotScopeWatermark: unifiedState.screenshotScopeWatermark,\n interpretationSafetyContract: unifiedState.interpretationSafetyContract,\n interpretationUxContract: {\n schema: unifiedState.interpretationUxContract?.schema,\n layerOrder: unifiedState.interpretationUxContract?.layerOrder,\n narrativeNeverVisuallyDominant: unifiedState.interpretationUxContract?.narrativeNeverVisuallyDominant\n },\n governance: {\n narrativeIsNotDecisionLayer: unifiedState.governance.narrativeIsNotDecisionLayer,\n displayOrder: unifiedState.governance.displayOrder,\n trustPosture: unifiedState.governance.trustPosture,\n interpretationUxContract: unifiedState.governance.interpretationUxContract?.schema\n },\n humanOps: {\n decisionTier: unifiedState.humanOps?.humanDecisionScaling?.tier,\n decisionOwner: unifiedState.humanOps?.humanDecisionScaling?.decisionOwner,\n opsRouteId: unifiedState.humanOps?.humanDecisionOpsRunbook?.routing?.routeId,\n slaAckMinutes: unifiedState.humanOps?.humanDecisionOpsRunbook?.sla?.ackMinutes,\n uiDisclaimerRequired: unifiedState.humanOps?.narrativeUiSafety?.displayRules?.requireDisclaimerBanner,\n misreadHighResidual: unifiedState.humanOps?.misreadSimulation?.highResidualCount,\n propagationHighResidual: unifiedState.humanOps?.socialPropagationSimulation?.highResidualCount,\n worstWatermarkSurvivability:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.worstWatermarkSurvivability,\n dominantDistortionSource:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.dominantDistortionSource\n },\n culturalRisk: {\n perceptualRiskClass: unifiedState.culturalRisk?.perceptualRiskClass,\n trustFork: unifiedState.culturalRisk?.trustDynamics?.fork,\n trustDecayScore: unifiedState.culturalRisk?.trustDynamics?.scores?.trustDecay,\n mythologyScore: unifiedState.culturalRisk?.trustDynamics?.scores?.mythology,\n confidenceDetachmentExposure:\n unifiedState.culturalRisk?.trustDynamics?.axes?.confidenceDetachmentExposure\n },\n appliedSystemsLayer: {\n schema: unifiedState.appliedSystemsLayer?.schema,\n researchOutputType: unifiedState.appliedSystemsLayer?.researchGovernance?.outputType,\n roboticsBlockActuation: unifiedState.appliedSystemsLayer?.roboticsGrounding?.blockActuation,\n spiralRumorCount:\n unifiedState.appliedSystemsLayer?.spiralMMOGameKernel?.gameLayer?.rumors?.length\n },\n actionSemanticGovernance: {\n schema: unifiedState.actionSemanticGovernance?.schema,\n highRiskActionCount: unifiedState.actionSemanticGovernance?.highRiskActionCount,\n loopClosed: unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.loopClosed,\n authorityCollapseMitigated:\n unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.risks?.authorityCollapse\n ?.mitigated\n },\n actionContextResolution: {\n schema: unifiedState.actionContextResolution?.schema,\n contextFingerprint: unifiedState.actionContextResolution?.contextFingerprint?.fingerprintHash,\n domainCollision: unifiedState.actionContextResolution?.domainCollision?.detected,\n deployAgentSpiralBlocked:\n unifiedState.actionContextResolution?.actionOverloadingRisk?.deployAgentExample\n ?.spiral_mmo === \"blocked\",\n roboticsActuationEligible:\n unifiedState.actionContextResolution?.roboticsActuationCorrectness?.actuationEligible\n },\n epistemicCoherence: {\n schema: unifiedState.epistemicCoherence?.schema,\n verdict: unifiedState.epistemicCoherence?.systemCoherence?.verdict,\n coherenceScore: unifiedState.epistemicCoherence?.systemCoherence?.score,\n contradictionCount: unifiedState.epistemicCoherence?.crossLayerContradictions?.count,\n operatorDecision: unifiedState.epistemicCoherence?.uxCompression?.operatorDecision,\n headlineTr: unifiedState.epistemicCoherence?.uxCompression?.headline?.tr,\n falseSafetyWarning: Boolean(\n unifiedState.epistemicCoherence?.uxCompression?.falseSafetyWarning\n )\n },\n coherenceAuthorityBoundary: {\n schema: unifiedState.coherenceAuthorityBoundary?.schema,\n bindingValid: unifiedState.coherenceAuthorityBoundary?.eclAuthorityBinding?.valid,\n canExecute: unifiedState.coherenceAuthorityBoundary?.contract?.can_execute,\n mandatoryBannerTr:\n unifiedState.coherenceAuthorityBoundary?.disclaimers?.mandatoryBanner?.tr\n },\n decisionLatencyGovernance: {\n schema: unifiedState.decisionLatencyGovernance?.schema,\n latencyTier: unifiedState.decisionLatencyGovernance?.latencyTier,\n inflationScore: unifiedState.decisionLatencyGovernance?.latencyInflation?.score,\n fastPathEligible: unifiedState.decisionLatencyGovernance?.fastPath?.eligible,\n primaryActionId: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.primaryActionId,\n ackSlaMinutes: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.ackSlaMinutes,\n certaintyCap: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.certaintyCap,\n uncertaintyItems:\n unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.uncertaintyEnvelope?.itemCount\n },\n decisionPacketUncertaintyBoundary: {\n schema: unifiedState.decisionPacketUncertaintyBoundary?.schema,\n invariantValid: unifiedState.decisionPacketUncertaintyBoundary?.invariantCheck?.valid,\n centralInvariant: unifiedState.decisionPacketUncertaintyBoundary?.centralInvariant\n },\n epistemicDecisionPacing: {\n schema: unifiedState.epistemicDecisionPacing?.schema,\n queueSaturationTier:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation?.tier,\n operatorProcessingLatency:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation\n ?.operatorProcessingLatency,\n postureWindowCount:\n unifiedState.epistemicDecisionPacing?.temporaryStaticPostureWindows\n ?.temporaryStaticPostureWindows?.windowCount,\n executionBoundariesValid:\n unifiedState.epistemicDecisionPacing?.executionBoundaries?.valid,\n grantsExecutionApproval: unifiedState.epistemicDecisionPacing?.grantsExecutionApproval\n },\n epistemicTemporalCoherence: {\n schema: unifiedState.epistemicTemporalCoherence?.schema,\n allInvariantsValid: unifiedState.epistemicTemporalCoherence?.allInvariantsValid,\n temporalAlignmentValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.temporalAlignment?.valid,\n crossWindowGuardValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard?.valid,\n delayedTruthValid: unifiedState.epistemicTemporalCoherence?.invariants?.delayedTruthRule?.valid,\n reconciliationRequired:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard\n ?.reconciliation?.required\n },\n realityDriftObserver: {\n schema: unifiedState.realityDriftObserver?.schema,\n driftActive: unifiedState.realityDriftObserver?.driftObserverSummary?.active,\n misapprehensionShapeCount:\n unifiedState.realityDriftObserver?.misapprehensionShapeCatalog?.shapeCount,\n criticalShapeCount:\n unifiedState.realityDriftObserver?.driftObserverSummary?.criticalShapeCount,\n roboticsMismatchActive:\n unifiedState.realityDriftObserver?.roboticsFeedbackMismatch?.active\n },\n driftCausality: {\n schema: unifiedState.driftCausality?.schema,\n shapesWithCausalExplanation: unifiedState.driftCausality?.rdolLinkage?.shapesWithCausalExplanation,\n activeDomainCount: unifiedState.driftCausality?.causalitySummary?.activeDomainCount,\n propagationCausalityActive: unifiedState.driftCausality?.causalDomains?.propagation?.active\n },\n productReadiness: unifiedState.productReadiness,\n stateLayers: {\n raw: unifiedState.stateLayers.raw,\n derived: {\n systemState: unifiedState.stateLayers.derived.systemState,\n narrative: unifiedState.stateLayers.derived.narrative,\n validation: unifiedState.stateLayers.derived.validation\n },\n policy: {\n binding: unifiedState.stateLayers.policy.binding,\n decisionOwner: unifiedState.stateLayers.policy.decisionOwner,\n prohibitedActions: unifiedState.stateLayers.policy.prohibitedActions\n }\n },\n systemState: unifiedState.systemState,\n interpretation: unifiedState.interpretation,\n validation: {\n trustPosture: unifiedState.validation.trustPosture,\n adjustedConfidence: unifiedState.validation.adjustedConfidence,\n agreement: unifiedState.validation.confidenceDecomposition.agreement,\n divergenceScore: unifiedState.validation.divergence.divergenceScore,\n validated: unifiedState.validation.divergence.validated,\n uncertaintyTags: unifiedState.validation.uncertainty.tags.map((t) => t.id),\n flags: unifiedState.validation.divergence.flags.map((f) => f.id)\n },\n compression: unifiedState.compression\n }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 6 * 1024 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (\n !checkHttpRateLimit(`rhizoh_voice_transcribe:${rlKey}`, RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN, 60_000)\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const voiceEnv = rhizohVoiceTranscribeEnvV3();\n if (!voiceEnv.any) {\n sendJson(res, 503, {\n ok: false,\n error: \"voice_asr_not_configured\",\n env: voiceEnv\n });\n return;\n }\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n const result = await runRhizohVoiceTranscribeV3(\n applyLanguagePropagationToVoiceBodyV1(payload, langProp)\n );\n sendJson(res, result.ok ? 200 : 422, {\n ...result,\n traceId: String(payload?.traceId || \"\"),\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n atMs: Date.now()\n });\n } catch (error) {\n sendJson(res, 500, {\n ok: false,\n error: \"voice_transcribe_failed\",\n detail: String(error?.message || error),\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(\"/rhizoh/llm/task/\")) {\n try {\n const rawTaskId = decodeURIComponent(pathname.slice(\"/rhizoh/llm/task/\".length).split(\"/\")[0] || \"\");\n const taskId = String(rawTaskId || \"\").toLowerCase() === \"last\" ? getLatestLlmWorkerTaskIdV0() : rawTaskId;\n const snap = taskId ? getLlmWorkerTaskSnapshotV0(taskId) : null;\n if (!snap) {\n sendJson(res, 404, {\n ok: false,\n error: \"llm_task_not_found\",\n taskId: rawTaskId,\n status: \"missing\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n if (snap.status === \"processing\") {\n sendJson(res, 202, {\n ok: true,\n taskId: snap.taskId,\n status: \"processing\",\n createdAt: snap.createdAt,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n // Terminal states (completed/failed) always HTTP 200 — poll must not look like server fault (500).\n sendJson(res, 200, {\n ...(snap.body || { ok: false, error: \"llm_task_empty\", taskId: snap.taskId }),\n taskId: snap.taskId,\n status: snap.status === \"completed\" ? \"completed\" : snap.status,\n terminal: true,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n } catch (error) {\n console.error(\"[llm-task-poll] handler failed:\", error);\n sendJson(res, 200, {\n ok: false,\n error: \"llm_task_poll_handler_failed\",\n detail: String(error?.message || error),\n status: \"failed\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === rhizohRuntime.routes.rhizohLlm) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 128 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n logRhizohHealth(\"gateway_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: String(payload?.traceId || \"\"),\n auth: auth.ok ? \"ok\" : \"anon\",\n ip\n });\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (!checkHttpRateLimit(`rhizoh_llm:${rlKey}`, RL_RHIZOH_LLM_PER_MIN, 60_000)) {\n const rlTaxonomy = classifyStressResponseV0({ code: \"rate_limit_exceeded\" });\n return sendJson(res, 429, {\n ok: false,\n error: \"rate_limit_exceeded\",\n reply: rlTaxonomy.userMessageTr || \"İstek sınırı aşıldı. Kısa süre sonra tekrar deneyin.\",\n stressClass: rlTaxonomy.stressClass,\n responseAction: rlTaxonomy.responseAction,\n stressMatrix: rlTaxonomy.matrix,\n stressInterpretable: rlTaxonomy.interpretable,\n stressConfidence: rlTaxonomy.stressConfidence,\n actionConfidence: rlTaxonomy.actionConfidence,\n actionSoftened: rlTaxonomy.actionSoftened,\n responseActionStrict: rlTaxonomy.responseActionStrict,\n actionInterpretable: rlTaxonomy.actionInterpretable,\n conflictResolution: rlTaxonomy.conflictResolution,\n directive: \"NONE\"\n });\n }\n\n const requireExplicit = process.env.CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE === \"1\";\n const keyMode = normalizeClientLlmKeySource(payload?.llmKeySource ?? payload?.keySource);\n if (keyMode === null) {\n return sendJson(res, 400, {\n ok: false,\n error: \"invalid_llm_key_source\",\n reply: \"llmKeySource geçersiz. Kullanın: env | user_connection | auto\",\n directive: \"NONE\"\n });\n }\n if (requireExplicit && keyMode === \"auto\") {\n return sendJson(res, 400, {\n ok: false,\n error: \"llm_key_source_required\",\n reply: \"Sunucu CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE=1: istekte llmKeySource (env veya user_connection) zorunlu.\",\n directive: \"NONE\"\n });\n }\n\n const conn = auth.ok ? await resolveConnection(auth.uid, String(payload?.connectionId || \"\")) : null;\n\n if (keyMode === \"user_connection\") {\n if (!auth.ok) {\n return sendJson(res, 401, {\n ok: false,\n error: \"llm_auth_required_for_user_connection\",\n reply: \"Kayıtlı LLM bağlantısı için giriş gerekli.\",\n directive: \"NONE\"\n });\n }\n if (!conn?.apiKey) {\n return sendJson(res, 400, {\n ok: false,\n error: \"user_llm_connection_required\",\n reply: \"Geçerli bir LLM bağlantısı yok; Studio’da bağlantı ekleyin veya llmKeySource=env kullanın.\",\n directive: \"NONE\"\n });\n }\n }\n\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n let safePayload = applyLanguagePropagationToLlmPayloadV1({ ...(payload || {}) }, langProp);\n const trimOut = trimRhizohLlmGatewayPayloadV1(safePayload);\n if (trimOut.meta.trimmed) {\n logRhizohHealth(\"llm_payload_trimmed\", {\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n bytesBefore: trimOut.meta.bytesBefore,\n bytesAfter: trimOut.meta.bytesAfter\n });\n }\n safePayload = trimOut.payload;\n delete safePayload.apiKey;\n delete safePayload.llmKeySource;\n delete safePayload.keySource;\n\n const llmMessage = String(\n safePayload?.message || safePayload?.text || payload?.message || payload?.text || \"\"\n ).trim();\n if (!llmMessage) {\n return sendJson(res, 400, {\n ok: false,\n error: \"message_required\",\n reply: \"Mesaj metni gerekli (message alanı boş).\",\n directive: \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n if (!safePayload.message) {\n safePayload = { ...safePayload, message: llmMessage };\n }\n\n let resolvedProvider = payload?.provider;\n let resolvedModel = payload?.model;\n let connApiKey = \"\";\n\n ({\n resolvedProvider,\n resolvedModel,\n connApiKey\n } = resolveServerLlmProviderV0(keyMode, conn, payload));\n\n const turnInput = {\n safePayload,\n auth,\n keyMode,\n conn,\n resolvedProvider,\n resolvedModel,\n connApiKey\n };\n const turnMeta = {\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n langEcho: buildRhizohLanguagePropagationEchoV1(langProp)\n };\n\n if (\n isLlmWorkerEnabledV0() &&\n isLlmWorkerAsyncEnabledV0() &&\n String(req.headers[\"x-rhizoh-llm-sync\"] || \"\").trim() !== \"1\"\n ) {\n const taskId = enqueueLlmWorkerTaskAsyncV0(turnInput, turnMeta);\n logRhizohHealth(\"llm_worker_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n taskId,\n traceId: String(payload?.traceId || taskId),\n keyMode,\n async: true\n });\n sendJson(res, 202, {\n ok: true,\n taskId,\n status: \"processing\",\n traceId: String(payload?.traceId || taskId),\n pollPath: `/rhizoh/llm/task/${taskId}`,\n gatewayPid: process.pid,\n ...turnMeta.langEcho\n });\n return;\n }\n\n const { result, traceId, turnLatencyMs, spinePhases, sampledTrace } = isLlmWorkerEnabledV0()\n ? await runLlmWorkerTaskV0(turnInput, turnMeta)\n : await rhizohGatewayTurn(turnInput);\n logRhizohHealth(\"llm_response\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId,\n turnLatencyMs: Number(turnLatencyMs || 0),\n keyMode\n });\n sendJson(res, 200, {\n ...result,\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n traceId,\n turnLatencyMs,\n ...(spinePhases ? { spinePhases } : {}),\n ...(sampledTrace !== undefined ? { sampledTrace } : {})\n });\n } catch (error) {\n const msg = String(error?.message || \"\");\n const code = error?.code || \"\";\n let status = 500;\n if (msg === \"payload_too_large\") status = 413;\n else if (msg === \"rate_limit_exceeded\") status = 429;\n else if (\n code === \"cost_hard_limit\" ||\n code === \"phased_rollout_capacity\" ||\n error?.containment === true ||\n String(code).startsWith(\"agent_\")\n ) {\n status = 429;\n } else if (code === \"user_soft_blocked\" || code === \"prompt_abuse_detected\") status = 403;\n else if (\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(code) ||\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(msg) ||\n msg.includes(\"missing_api_key_for_\")\n )\n status = 400;\n let rhizohFailureKind = \"provider_error\";\n if (msg === \"rate_limit_exceeded\" || status === 429) rhizohFailureKind = \"rate_limit\";\n else if (status === 403) rhizohFailureKind = \"policy_block\";\n else if (status === 400 || status === 413) rhizohFailureKind = \"client_config\";\n else if (msg.startsWith(\"provider_http_\")) {\n const st = Number(msg.replace(\"provider_http_\", \"\"));\n if (st === 408 || st === 504) rhizohFailureKind = \"timeout\";\n else rhizohFailureKind = \"provider_error\";\n } else if (/timeout|timed out|ETIMEDOUT/i.test(msg)) rhizohFailureKind = \"timeout\";\n\n const providerHttpMatch = msg.match(/^provider_http_(\\d{3})$/);\n const stressTaxonomy =\n error?.stressClass != null\n ? {\n stressClass: error.stressClass,\n responseAction: error.responseAction,\n stressMatrix: error.stressMatrix,\n stressInterpretable: error.stressInterpretable !== false,\n stressConfidence: error.stressConfidence,\n actionConfidence: error.actionConfidence,\n actionSoftened: error.actionSoftened,\n responseActionStrict: error.responseActionStrict,\n actionInterpretable: error.actionInterpretable,\n conflictResolution: error.conflictResolution,\n stressSecondary: error.stressSecondary,\n responseActions: error.responseActions\n }\n : (() => {\n const t = classifyStressResponseV0({\n code: code || msg,\n rhizohFailureKind,\n providerHttpStatus: providerHttpMatch\n ? Number(providerHttpMatch[1])\n : null\n });\n return {\n stressClass: t.stressClass,\n responseAction: t.responseAction,\n stressMatrix: t.matrix,\n stressInterpretable: t.interpretable,\n stressConfidence: t.stressConfidence,\n actionConfidence: t.actionConfidence,\n actionSoftened: t.actionSoftened,\n responseActionStrict: t.responseActionStrict,\n actionInterpretable: t.actionInterpretable,\n conflictResolution: t.conflictResolution,\n stressSecondary: t.stressSecondary,\n responseActions: t.responseActions\n };\n })();\n sendJson(res, status, {\n ok: false,\n rhizohFailureKind,\n ...(providerHttpMatch ? { providerHttpStatus: Number(providerHttpMatch[1]) } : {}),\n error: code || msg || \"rhizoh_llm_failed\",\n detail: msg || code || undefined,\n reply:\n error?.reply ||\n (status === 500 ? \"Rhizoh bağlantısı geçici olarak kesildi.\" : msg),\n directive: error?.directive || \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n ...stressTaxonomy\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicSeal) {\n try {\n if (!EPISTEMIC_SEAL_SECRET) {\n sendJson(res, 503, { ok: false, error: \"epistemic_seal_disabled\" });\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`epistemic_seal:${ip}`, RL_EPISTEMIC_SEAL_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const truth = body.truth_contract;\n if (!truth || typeof truth !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"truth_contract_required\" });\n return;\n }\n const sealBody = {\n truth_contract: truth,\n runtime_hash: String(body.runtime_hash || \"\"),\n model_route: {\n provider: body.model_route?.provider ?? null,\n model: body.model_route?.model ?? null\n },\n memory_digest: String(body.memory_digest || \"\"),\n world_snapshot_hash: String(body.world_snapshot_hash || \"\"),\n timestamp: Number(body.timestamp) || Date.now()\n };\n const canonical = canonicalEpistemicSealString(sealBody);\n const { hash, signature } = hashAndSignEpistemicSeal(canonical, EPISTEMIC_SEAL_SECRET);\n recordGenesisEpistemicSealIssued(hash);\n const attestation = {\n schema: EPISTEMIC_SEAL_SCHEMA,\n algorithm: \"SHA-256+HMAC-SHA256\",\n keyHint: process.env.CASTLE_EPISTEMIC_SEAL_SECRET ? \"CASTLE_EPISTEMIC_SEAL_SECRET\" : \"CASTLE_GATEWAY_TOKEN\",\n issuedAt: Date.now(),\n gateway: { service: \"castle-gateway\", listenPort: PORT }\n };\n sendJson(res, 200, {\n ok: true,\n hash,\n signature,\n attestation,\n canonicalBytes: Buffer.byteLength(canonical, \"utf8\")\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"seal_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicLogsBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`epistemic_logs:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 120) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const persisted = await persistEpistemicLedgerBatch(auth.uid, entries);\n recordGenesisEpistemicLedgerPersisted(persisted.persisted);\n // Internal cognition layer: forecast stays gateway/ledger-only (no client exposure).\n try {\n await persistEpistemicForecastBatch(auth.uid, persisted.normalized || []);\n } catch {\n /* forecast failures must not break primary ledger ingest */\n }\n sendJson(res, 200, {\n ok: true,\n persisted: persisted.persisted,\n mode: persisted.mode,\n latest: persisted.normalized?.[persisted.normalized.length - 1] || null\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"epistemic_log_ingest_failed\") });\n }\n return;\n }\n\n sendJson(res, 404, { ok: false, error: \"not_found\" });\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\nspiralWssForGenesis = wss;\nconst orchestrator = createOrchestrator();\nconst clientStats = new Map();\nlet broadcasterClientId = null;\nconst spiralState = {\n rooms: [{ id: \"room-default\", name: \"Spiral Default Arena\" }],\n activeByClient: new Map(),\n characters: new Map() // id -> {id,name,role,roomId,source}\n};\n\n/** Castle social rooms — keyed by `castleRoomKey` from {@link WS_MESSAGE.CASTLE_SOCIAL_PULSE}. */\nconst castleSocialRoomState = new Map();\nconst CASTLE_SOCIAL_MEMBER_TTL_MS = 90_000;\nconst CASTLE_SOCIAL_MAX_PULSE_BYTES = 8192;\nconst RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES = 5 * 1024 * 1024;\nconst RHIZOH_VOICE_LIVE_MAX_CHUNKS = 96;\n\n/**\n * Voice Engine v3 live media lane.\n * Client streams MediaRecorder chunks over the existing gateway WS; STOP reuses the\n * gateway ASR runner so orchestration remains the single authority downstream.\n */\nconst rhizohVoiceLiveSessions = new WeakMap();\n\nfunction appendRhizohVoiceLiveEventV0(session, type, payloadRefSeed = \"\") {\n if (!session) return null;\n session.eventSeq = (Number(session.eventSeq) || 0) + 1;\n const event = createRhizohImmutableEventV0({\n type,\n sessionId: session.sessionId,\n traceId: session.traceId,\n actorSeed: session.sessionId || session.traceId,\n eventSeq: session.eventSeq,\n payloadRef: createRhizohPayloadRefV0(payloadRefSeed || `${session.sessionId}:${type}:${session.eventSeq}`)\n });\n session.events = [...(session.events || []), event].slice(-64);\n return event;\n}\n\nfunction sendRhizohVoiceLiveEnvelopeV0(socket, type, payload) {\n try {\n if (socket?.readyState === 1) socket.send(JSON.stringify(createEnvelope(type, payload)));\n } catch {\n /* noop */\n }\n}\n\nfunction decodeRhizohVoiceLiveChunkV0(payload = {}) {\n const b64 = String(payload.audioBase64 || \"\").trim();\n if (!b64) return { ok: false, error: \"audio_base64_required\" };\n try {\n const buf = Buffer.from(b64, \"base64\");\n if (!buf.length) return { ok: false, error: \"audio_decode_empty\" };\n return { ok: true, buffer: buf };\n } catch {\n return { ok: false, error: \"audio_base64_invalid\" };\n }\n}\n\nfunction handleRhizohVoiceLiveStartV0(socket, payload = {}) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const sessionId = String(p.sessionId || `live_${Date.now().toString(36)}`).slice(0, 96);\n rhizohVoiceLiveSessions.set(socket, {\n sessionId,\n traceId: String(p.traceId || \"\").slice(0, 128),\n mimeType: String(p.mimeType || \"audio/webm\").slice(0, 80),\n languageCode: String(p.languageCode || \"tr-TR\").slice(0, 32),\n path: String(p.path || \"both\").slice(0, 24),\n chunks: [],\n chunkIndexes: [],\n totalBytes: 0,\n nextChunkIndex: 1,\n eventSeq: 0,\n events: [],\n startedAtMs: Date.now()\n });\n appendRhizohVoiceLiveEventV0(rhizohVoiceLiveSessions.get(socket), \"VOICE_LIVE_SESSION_STARTED\", `${sessionId}:live_session`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_START, {\n ok: true,\n sessionId,\n schema: \"castle.rhizoh.voice_live.gateway_lane.v0\"\n });\n}\n\nfunction handleRhizohVoiceLiveChunkV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const sequence = validateRhizohVoiceLiveChunkPayloadV0(payload, {\n expectedChunkIndex: session.nextChunkIndex\n });\n if (!sequence.ok) {\n rhizohVoiceLiveSessions.delete(socket);\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:chunk_reject`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: sequence.error,\n expectedChunkIndex: sequence.expectedChunkIndex,\n chunkIndex: sequence.chunkIndex\n });\n return;\n }\n const decoded = decodeRhizohVoiceLiveChunkV0(payload);\n if (!decoded.ok) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: decoded.error\n });\n return;\n }\n if (\n session.chunks.length >= RHIZOH_VOICE_LIVE_MAX_CHUNKS ||\n session.totalBytes + decoded.buffer.length > RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES\n ) {\n rhizohVoiceLiveSessions.delete(socket);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_audio_too_large\"\n });\n return;\n }\n session.chunks.push(decoded.buffer);\n session.chunkIndexes.push(sequence.chunkIndex);\n session.totalBytes += decoded.buffer.length;\n session.nextChunkIndex += 1;\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_CHUNK_ACCEPTED\", `${session.sessionId}:chunk:${sequence.chunkIndex}`);\n}\n\nfunction handleRhizohVoiceLiveStopV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n rhizohVoiceLiveSessions.delete(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const lastChunkIndex = Number(payload?.lastChunkIndex);\n if (Number.isInteger(lastChunkIndex) && lastChunkIndex !== session.chunks.length) {\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:stop_mismatch`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_chunk_count_mismatch\",\n expectedChunkCount: lastChunkIndex,\n receivedChunkCount: session.chunks.length\n });\n return;\n }\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_STOP_RECEIVED\", `${session.sessionId}:stop:${session.chunks.length}`);\n const audio = Buffer.concat(session.chunks);\n if (!audio.length) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"audio_empty\"\n });\n return;\n }\n runRhizohVoiceTranscribeV3({\n path: String(payload?.path || session.path || \"both\"),\n audioBase64: audio.toString(\"base64\"),\n mimeType: String(payload?.mimeType || session.mimeType || \"audio/webm\"),\n languageCode: String(payload?.languageCode || session.languageCode || \"tr-TR\"),\n traceId: String(payload?.traceId || session.traceId || \"\"),\n sessionId: session.sessionId,\n transport: \"gateway_ws_live\"\n })\n .then((result) => {\n appendRhizohVoiceLiveEventV0(session, result?.ok ? \"VOICE_TRANSCRIBE_FINAL\" : \"VOICE_TRANSCRIBE_FAILED\", `${session.sessionId}:final`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_FINAL, {\n ...result,\n sessionId: session.sessionId,\n transportPath: \"gateway_ws_live\",\n bytes: audio.length,\n chunkCount: session.chunks.length,\n chunkSequence: Object.freeze({\n ok: true,\n firstChunkIndex: session.chunkIndexes[0] || null,\n lastChunkIndex: session.chunkIndexes[session.chunkIndexes.length - 1] || null,\n receivedChunkCount: session.chunks.length\n }),\n eventTrace: Object.freeze((session.events || []).slice(-16)),\n recordedMs: Math.max(0, Date.now() - session.startedAtMs)\n });\n })\n .catch((e) => {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_transcribe_failed\",\n detail: String(e?.message || e)\n });\n });\n}\n\nfunction removeCastleSocialClientFromAllRooms(clientId) {\n const cid = String(clientId || \"\");\n if (!cid) return;\n for (const room of castleSocialRoomState.values()) {\n room.members.delete(cid);\n }\n}\n\nfunction buildCastleSocialRoomPayload(roomKey) {\n const room = castleSocialRoomState.get(roomKey);\n if (!room) return null;\n const now = Date.now();\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const roster = [...room.members.values()].map((m) => ({\n userId: m.userId,\n lastMs: m.lastMs,\n ...m.sp\n }));\n const peerFeeds =\n room.walPeers && room.walPeers.size\n ? [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }))\n : [];\n return { castleRoomKey: roomKey, seq: room.seq, roster, peerFeeds, ts: now };\n}\n\nconst CASTLE_WAL_PEER_MAX_FEED_BYTES = 48 * 1024;\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {{ ok: boolean, code?: string, reason?: string }} gate\n */\nfunction rejectWalPeerIngressV0(socket, gate) {\n const code = gate.code || \"wal_peer_rejected\";\n recordWalPeerRejectV0(code);\n recordSubstrateOtelEventV0(\"wal_peer_reject\", { code, reason: String(gate.reason || \"\").slice(0, 120) });\n socket.send(\n JSON.stringify(\n createEnvelope(WS_MESSAGE.ERROR, {\n error: gate.reason || \"WAL peer ingress rejected.\",\n code\n })\n )\n );\n}\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {Record<string, unknown> | null | undefined} walPeerFeed\n * @returns {boolean}\n */\nfunction gateWalPeerIngressV0(socket, walPeerFeed) {\n const authGate = assertWalPeerSocketAuthorityV0(socket);\n if (!authGate.ok) {\n rejectWalPeerIngressV0(socket, authGate);\n return false;\n }\n const sigGate = validateWalPeerFeedSignatureV0(\n walPeerFeed && typeof walPeerFeed === \"object\" ? walPeerFeed : {}\n );\n if (!sigGate.ok) {\n rejectWalPeerIngressV0(socket, sigGate);\n return false;\n }\n return true;\n}\n\nfunction handleCastleWalPeerFeed(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_WAL_PEER_MAX_FEED_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_WAL_PEER_FEED too large.\" })));\n return;\n }\n const walPeerFeed = p.walPeerFeed && typeof p.walPeerFeed === \"object\" ? p.walPeerFeed : {};\n if (!gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const castleRoomKey = String(p.castleRoomKey || \"wal:convergence\").slice(0, 64);\n const castleId = String(p.castleId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId,\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: room.members.get(socket.clientId)?.sp ?? {}\n });\n const peerFeeds = [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }));\n const body = {\n castleRoomKey,\n seq: room.seq,\n peerFeeds,\n ts: now\n };\n const env = createEnvelope(WS_MESSAGE.CASTLE_WAL_PEER_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys(), ...room.walPeers.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction handleCastleSocialPulse(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_SOCIAL_MAX_PULSE_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_SOCIAL_PULSE too large.\" })));\n return;\n }\n const castleRoomKey = String(p.castleRoomKey || \"default\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n const sp = p.socialPulse && typeof p.socialPulse === \"object\" ? p.socialPulse : {};\n const walPeerFeed =\n p.walPeerFeed && typeof p.walPeerFeed === \"object\"\n ? p.walPeerFeed\n : sp.walPeerFeed && typeof sp.walPeerFeed === \"object\"\n ? sp.walPeerFeed\n : null;\n if (walPeerFeed && !gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const spNext = walPeerFeed ? { ...sp, walPeerFeed } : { ...sp };\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: spNext\n });\n if (walPeerFeed) {\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId: String(p.castleId || userId).slice(0, 64),\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n }\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const body = buildCastleSocialRoomPayload(castleRoomKey);\n if (!body) return;\n const env = createEnvelope(WS_MESSAGE.CASTLE_SOCIAL_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcast(msgObj) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcastState() {\n broadcast(\n createEnvelope(WS_MESSAGE.BROADCAST_STATE, {\n broadcasterClientId,\n viewers: [...wss.clients].filter((c) => c.readyState === 1).length\n })\n );\n}\n\nfunction currentSpiralStatePayload() {\n return {\n rooms: spiralState.rooms,\n characters: [...spiralState.characters.values()]\n };\n}\n\nfunction broadcastSpiralState() {\n broadcast(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload()));\n}\n\nfunction ensureRoom(roomId, fallbackName = null) {\n if (!roomId) return spiralState.rooms[0].id;\n const exists = spiralState.rooms.find((r) => r.id === roomId);\n if (exists) return exists.id;\n const created = { id: roomId, name: fallbackName || `Spiral-${roomId.slice(-4)}` };\n spiralState.rooms.push(created);\n return created.id;\n}\n\nfunction syncSpiralCharactersWithWorld(snapshot) {\n const agents = snapshot?.agents || [];\n for (const a of agents) {\n if (!spiralState.characters.has(a.id)) {\n spiralState.characters.set(a.id, {\n id: a.id,\n name: a.id,\n role: \"agent\",\n roomId: spiralState.rooms[0].id,\n source: \"world\"\n });\n }\n }\n}\n\nfunction broadcastExcept(msgObj, exceptSocket) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client !== exceptSocket && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction validateInputFrame(payload, socket) {\n const stats = clientStats.get(socket.clientId) || { lastFrame: 0, windowStart: Date.now(), msgCount: 0 };\n const now = Date.now();\n if (now - stats.windowStart > 5000) {\n stats.windowStart = now;\n stats.msgCount = 0;\n }\n stats.msgCount += 1;\n clientStats.set(socket.clientId, stats);\n if (stats.msgCount > 120) return \"Rate limit exceeded.\";\n\n if (!payload || !Number.isInteger(payload.frame)) return \"frame must be integer.\";\n if (payload.frame <= stats.lastFrame) return \"frame must be monotonic.\";\n const commands = Array.isArray(payload.commands) ? payload.commands : [];\n if (commands.length > 8) return \"Too many commands in one frame.\";\n for (const cmd of commands) {\n if (!cmd || typeof cmd.command !== \"string\") return \"Invalid command format.\";\n if (\n cmd.command !== \"SPAWN_AGENT\" &&\n cmd.command !== \"LIST_CASTLES\" &&\n cmd.command !== \"SPAWN_ENTITY\" &&\n cmd.command !== \"CREATE_CASTLE\"\n ) return \"Unsupported command.\";\n }\n stats.lastFrame = payload.frame;\n clientStats.set(socket.clientId, stats);\n return null;\n}\n\nfunction validateSignalPayload(payload) {\n if (!payload || typeof payload !== \"object\") return \"Invalid signaling payload.\";\n const signalType = payload.signalType;\n if (![\"OFFER\", \"ANSWER\", \"ICE\"].includes(signalType)) return \"Invalid signal type.\";\n if (typeof payload.to !== \"string\" || payload.to.length < 3) return \"Invalid signal destination.\";\n if (signalType !== \"ICE\" && JSON.stringify(payload.sdp || {}).length > 16 * 1024) return \"SDP too large.\";\n if (signalType === \"ICE\" && JSON.stringify(payload.candidate || {}).length > 6 * 1024) return \"ICE too large.\";\n return null;\n}\n\nfunction sanitizeRawMessage(raw) {\n if (!raw) return null;\n const text = raw.toString();\n if (Buffer.byteLength(text, \"utf8\") > MAX_MESSAGE_BYTES) return null;\n return text;\n}\n\nwss.on(\"connection\", async (socket, req) => {\n const origin = req?.headers?.origin || \"\";\n if (!httpCors.isWebSocketOriginAllowed(origin)) {\n socket.close(1008, \"Origin not allowed\");\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const path = req?.url || \"\";\n const query = new URL(`ws://localhost${path}`).searchParams;\n const token = query.get(\"token\");\n if (!token || token !== REQUIRED_GATEWAY_TOKEN) {\n socket.close(1008, \"Invalid gateway token\");\n return;\n }\n }\n if (REQUIRE_AUTH) {\n const result = await verifyClientToken(req);\n if (!result.ok) {\n const isLocal = origin.includes(\"localhost\") || origin.includes(\"127.0.0.1\") || !origin;\n if (!(ALLOW_DEV_ANON && isLocal)) {\n socket.close(1008, result.reason);\n return;\n }\n socket.auth = { ok: true, kind: \"dev-anon\", user: { uid: \"dev-anon\" } };\n } else {\n socket.auth = result;\n }\n }\n\n socket.clientId = `c-${Math.random().toString(36).slice(2, 8)}`;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.HELLO, { message: \"Castle Gateway online.\", clientId: socket.clientId })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.WORLD_SNAPSHOT, orchestrator.worldSnapshot())));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.BROADCAST_STATE, { broadcasterClientId, viewers: [...wss.clients].filter((c) => c.readyState === 1).length })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload())));\n broadcastExcept(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }), socket);\n\n socket.on(\"message\", (raw) => {\n const safeRaw = sanitizeRawMessage(raw);\n if (!safeRaw) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Message too large or invalid.\" })));\n return;\n }\n const parsed = safeJsonParse(safeRaw);\n if (!parsed?.type) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid message.\" })));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND_TEXT) {\n const canonical = parseTextToCommand(parsed.payload?.text);\n if (!canonical) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Unknown command text.\" })));\n return;\n }\n const result = orchestrator.applyCommand(canonical);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND) {\n const result = orchestrator.applyCommand(parsed.payload);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n }\n\n if (parsed.type === WS_MESSAGE.INPUT_FRAME) {\n const err = validateInputFrame(parsed.payload, socket);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const frame = parsed.payload.frame;\n const commands = Array.isArray(parsed.payload?.commands) ? parsed.payload.commands : [];\n orchestrator.queueInputFrame(socket.clientId, frame, commands);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, queued: true, frame })));\n }\n\n if (parsed.type === WS_MESSAGE.SIGNAL) {\n const payload = parsed.payload || {};\n const err = validateSignalPayload(payload);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const signalType = payload.signalType;\n const to = payload.to;\n for (const client of wss.clients) {\n if (client.clientId === to && client.readyState === 1) {\n client.send(JSON.stringify(createEnvelope(WS_MESSAGE.SIGNAL, { ...payload, from: socket.clientId })));\n break;\n }\n }\n }\n\n if (parsed.type === WS_MESSAGE.DRONE_TELEMETRY) {\n const payload = parsed.payload || {};\n const json = JSON.stringify(payload);\n if (Buffer.byteLength(json, \"utf8\") > 48 * 1024) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"DRONE_TELEMETRY payload too large.\" })));\n return;\n }\n broadcastExcept(createEnvelope(WS_MESSAGE.DRONE_TELEMETRY, { ...payload, relayFrom: socket.clientId }), socket);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_START) {\n handleRhizohVoiceLiveStartV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_CHUNK) {\n handleRhizohVoiceLiveChunkV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_STOP) {\n handleRhizohVoiceLiveStopV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_SOCIAL_PULSE) {\n handleCastleSocialPulse(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_WAL_PEER_FEED) {\n handleCastleWalPeerFeed(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.OPEN_DATA_QUERY) {\n queryOpenData(parsed.payload)\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: true, result })));\n })\n .catch((e) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: false, error: e.message })));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_QUERY) {\n runRhizohBrainV2({\n ...(parsed.payload || {}),\n uid: socket?.auth?.user?.uid || \"unknown\"\n })\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, result)));\n })\n .catch(async () => {\n const fallback = await runRhizohBrain(parsed.payload || {}).catch(() => ({\n ok: false,\n reply: \"Rhizoh temporary disturbance. Habitat remains stable.\"\n }));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, fallback)));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.BROADCAST_REGISTER) {\n const role = String(parsed.payload?.role || \"\");\n if (role === \"GENESIS_BROADCAST_AGENT\") {\n broadcasterClientId = socket.clientId;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, registered: role })));\n broadcastState();\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.STUDIO_CUE) {\n // Only registered broadcaster can push global cues.\n if (!broadcasterClientId || socket.clientId !== broadcasterClientId) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Only broadcaster agent can emit STUDIO_CUE.\" })));\n return;\n }\n const cue = parsed.payload || {};\n if (typeof cue !== \"object\") {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid STUDIO_CUE payload.\" })));\n return;\n }\n broadcast(createEnvelope(WS_MESSAGE.STUDIO_CUE, cue));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_JOIN_ROOM) {\n const requested = String(parsed.payload?.roomId || \"\").trim();\n const requestedName = String(parsed.payload?.roomName || \"\").trim();\n const roomId = ensureRoom(requested || `room-${Math.random().toString(36).slice(2, 7)}`, requestedName || null);\n spiralState.activeByClient.set(socket.clientId, roomId);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, roomId })));\n broadcastSpiralState();\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_CREATE_CHARACTER) {\n const name = String(parsed.payload?.name || \"\").trim().slice(0, 32) || `AI-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;\n const role = String(parsed.payload?.role || \"agent\").trim().slice(0, 24) || \"agent\";\n const roomId = ensureRoom(\n String(parsed.payload?.roomId || spiralState.activeByClient.get(socket.clientId) || spiralState.rooms[0].id),\n null\n );\n\n // Character creation always maps to an authoritative world agent.\n const result = orchestrator.applyCommand({\n command: COMMAND.SPAWN_AGENT,\n params: { targetCastleId: \"castle-01\" }\n });\n if (!result?.ok || !result?.data?.id) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Character creation failed.\" })));\n return;\n }\n const id = result.data.id;\n spiralState.characters.set(id, { id, name, role, roomId, source: \"spawned\" });\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, characterId: id, roomId })));\n broadcastSpiralState();\n return;\n }\n });\n\n socket.on(\"close\", () => {\n if (socket.clientId === broadcasterClientId) broadcasterClientId = null;\n clientStats.delete(socket.clientId);\n rhizohVoiceLiveSessions.delete(socket);\n spiralState.activeByClient.delete(socket.clientId);\n removeCastleSocialClientFromAllRooms(socket.clientId);\n broadcast(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }));\n broadcastState();\n });\n});\n\nsetInterval(() => {\n void (async () => {\n try {\n const { frame, snapshot, delta, packedDelta } = orchestrator.tick(1 / 20);\n syncSpiralCharactersWithWorld(snapshot);\n broadcast(createEnvelope(WS_MESSAGE.NETWORK_TICK, { frame }));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_TICK, snapshot));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA, delta));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA_PACKED, packedDelta));\n broadcastSpiralState();\n await runAcademyEventTick();\n const executed = await tickAutomations(Date.now());\n for (const a of executed) {\n await addTranscript(a.uid, {\n source: \"planner\",\n eventType: \"automation-executed\",\n text: `Automation executed: ${a.title} (${a.action})`,\n roomId: \"studio-main\",\n meta: { automationId: a.id, action: a.action, deviceId: a.deviceId || \"\" }\n });\n }\n } catch (e) {\n console.error(\"[GATEWAY] world tick / automations loop error:\", String(e?.message || e));\n }\n })();\n}, 50);\n\n(async () => {\n await rhizohRuntime.telemetry.initOpenTelemetry();\n if (String(process.env.CASTLE_GENESIS_DISK_PERSIST ?? \"\").trim() === \"\") {\n process.env.CASTLE_GENESIS_DISK_PERSIST = \"1\";\n }\n const genesisDiskBoot = await hydrateGenesisContinuityPersistenceBootV0();\n if (!genesisDiskBoot.ok) {\n console.warn(\"[GATEWAY] genesis disk hydrate failed; continuing with in-memory genesis continuity\");\n }\n httpServer.listen(PORT, () => {\n startGenesisCanonicalClock();\n installGenesisCheckpointSurfaceGetter(buildGenesisRuntimeSurfacePayloadLive);\n setGenesisContinuityAfterPublishHook((seq) => {\n noteGenesisCheckpointSeqCommitted(seq);\n noteGenesisContinuitySeqAcceptedV0(seq);\n });\n startGenesisContinuityInfraSampler(buildGenesisRuntimeSurfacePayloadLive, 2000);\n logProductionObservatorySurfaceGuardsV0();\n logGatewaySubstrateAuthorityGuardsV0();\n if (isLlmWorkerEnabledV0()) {\n try {\n const workerBoot = initLlmWorkerAtBootV0();\n console.log(\"[GATEWAY] llm worker boot:\", workerBoot);\n } catch (workerErr) {\n console.error(\"[GATEWAY] llm worker boot failed:\", workerErr);\n }\n }\n console.log(`[GATEWAY] ws/http://localhost:${PORT}`);\n console.log(\n `[GATEWAY] genesis continuity: GET ${rhizohRuntime.routes.genesisRuntime} | POST ${rhizohRuntime.routes.genesisIngress} | SSE ${rhizohRuntime.routes.genesisStream} | replay ${rhizohRuntime.routes.genesisReplay} | diff ${rhizohRuntime.routes.genesisReplayDiff} | equiv ${rhizohRuntime.routes.genesisReplayEquivalence} | analytics ${rhizohRuntime.routes.genesisReplayAnalytics} | evolution ${rhizohRuntime.routes.genesisReplayEvolution}`\n );\n if (String(process.env.CASTLE_GENESIS_OBSERVATORY_LIVE_V01 ?? \"\").trim() === \"1\") {\n console.log(\"GENESIS OBSERVATORY LIVE v0.1 — READ ONLY PHASE STARTED\");\n }\n });\n})().catch((e) => {\n console.error(\"[GATEWAY] fatal boot (before or during listen):\", e);\n process.exit(1);\n});\n"}, "a2c8eb0d51afc35429a3dd926044594d096c3c6f": {"t": 1785754141.4146988, "n": 168613, "h": "d51df7d07528ac56", "c": "import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport dotenv from \"dotenv\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconsole.log(\"🔥 [GENESIS_BOOT] entry file:\", __filename);\nconsole.log(\"🔥 [GENESIS_BOOT] node env:\", process.env.NODE_ENV);\nconsole.log(\"🔥 [GENESIS_BOOT] pid:\", process.pid);\nconsole.log(\"🧬 [GENESIS_BOOT] cwd:\", process.cwd());\nconsole.log(\"🧬 [GENESIS_BOOT] import.meta.url:\", import.meta.url);\ndotenv.config({ path: path.join(__dirname, \"..\", \".env\") });\ndotenv.config({ path: path.join(__dirname, \"..\", \".env.local\"), override: true });\n\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport { createServer } from \"node:http\";\nimport { createOrchestrator } from \"@castle/orchestrator\";\nimport { parseTextToCommand } from \"@castle/command-dsl\";\nimport {\n WS_MESSAGE,\n COMMAND,\n createEnvelope,\n createRhizohImmutableEventV0,\n createRhizohPayloadRefV0,\n safeJsonParse,\n validateRhizohVoiceLiveChunkPayloadV0\n} from \"@castle/protocol\";\nimport { queryOpenData } from \"./openData.js\";\nimport { verifyClientToken } from \"./auth.js\";\nimport { runRhizohBrain } from \"./rhizohBrain.js\";\nimport { queryRhizohLlm } from \"./rhizohLlmGateway.js\";\nimport { rhizohGatewayTurn } from \"./rhizohGatewayTurn.js\";\nimport {\n enqueueLlmWorkerTaskAsyncV0,\n getLatestLlmWorkerTaskIdV0,\n getLlmWorkerDebugSnapshotV0,\n getLlmWorkerTaskSnapshotV0,\n initLlmWorkerAtBootV0,\n isLlmWorkerAsyncEnabledV0,\n isLlmWorkerEnabledV0,\n runLlmWorkerTaskV0\n} from \"./llmWorkerRuntime.js\";\nimport {\n RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n runRhizohVoiceTranscribeV3,\n rhizohVoiceTranscribeEnvV3\n} from \"./rhizohVoiceTranscribeV3.js\";\nimport { createHttpCorsPolicy, normalizeHttpOrigin, parseAllowedOriginsFromEnv } from \"./httpCorsPolicyV1.js\";\nimport {\n applyLanguagePropagationToLlmPayloadV1,\n applyLanguagePropagationToVoiceBodyV1,\n buildRhizohLanguagePropagationEchoV1,\n parseRhizohLanguagePropagationV1\n} from \"./rhizohLanguagePropagationV1.js\";\nimport { trimRhizohLlmGatewayPayloadV1 } from \"./rhizohLlmPayloadTrimV1.js\";\nimport {\n meshAppendDelta,\n meshContinuityAggregate,\n meshJoin,\n meshLeave,\n meshReplayFromNodeId,\n meshReplayFromSeq,\n meshSnapshot,\n meshSubscribeSse\n} from \"./presenceRoomMesh.js\";\nimport { runRhizohBrainV2 } from \"./rhizohBrainV2.js\";\nimport {\n listConnections,\n createConnection,\n updateConnection,\n deleteConnection,\n setDefaultConnection,\n resolveConnection\n} from \"./llmConnectionsStore.js\";\nimport { checkHttpRateLimit, getHttpClientIp } from \"./castleHttpRateLimit.js\";\nimport {\n submitAbuseReportV0,\n listModerationQueueV0,\n softBlockUserV0,\n removeSoftBlockV0,\n listSoftBlockedUsersV0\n} from \"./ops/moderationMvpV0.js\";\nimport { listRecentAgentSnapshotsV0 } from \"./ops/agentObservabilityV0.js\";\nimport { readAgentContainmentConfigV0 } from \"./ops/agentContainmentV0.js\";\nimport { readCostContainmentConfigV0, getCostContainmentStatsV0 } from \"./ops/costContainmentV0.js\";\nimport { getGlobalCostLedgerSnapshotV0, getGclBackendStatusV0 } from \"./ops/globalCostLedgerV0.js\";\nimport { evaluateEconomicStrategyL3V0 } from \"./ops/economicStrategyEngineL3V0.js\";\nimport { getPhasedRolloutStatsV0, readPhasedRolloutTierV0 } from \"./ops/phasedRolloutV0.js\";\nimport { buildUnifiedStateNarrativeV0 } from \"./ops/unifiedStateNarrativeV0.js\";\nimport { resolveHardeningTenantIdV0 } from \"./ops/narrativeTenantIsolationV0.js\";\nimport {\n classifyStressResponseV0,\n verifyStressTaxonomyCoverageV0,\n verifyStressConflictResolutionV0\n} from \"./ops/stressResponseTaxonomyV0.js\";\nimport { verifyResolutionStabilityEnvelopeV0 } from \"./ops/resolutionStabilityEnvelopeV0.js\";\nimport { logLlmAccess } from \"./castleLlmAudit.js\";\nimport { appendMemory, listMemories, getMemoryContext, getPersonaGoalMemory, setPersonaGoalMemory, autoCompactMemories } from \"./memoryStore.js\";\nimport { getFirebasePersistence } from \"./firebasePersistence.js\";\nimport { registerAgentIdentity, listAgentIdentities, getAgentIdentity, updateAgentIdentity } from \"./agentIdentityStore.js\";\nimport { queueAcademyEvent, listAcademyEvents, resolveAcademyEvent, runAcademyEventTick } from \"./academyEventEngine.js\";\nimport { enqueuePdfJob, getPdfJob } from \"./pdfPipeline.js\";\nimport {\n createPublishSession,\n listPublishSessions,\n updatePublishSession,\n addTranscript,\n listTranscripts,\n searchMetadata,\n registerDevice,\n listDevices,\n updateDevice,\n appendTelemetry,\n listTelemetry,\n createAutomation,\n listAutomations,\n tickAutomations,\n enqueueCommand,\n listCommands,\n updateCommandStatus,\n registerSocialChannel,\n listSocialChannels\n} from \"./studioOpsStore.js\";\nimport { metrics as infraMetrics } from \"./infra/metrics.js\";\nimport { renderRhizohPrometheusMetrics, rhizohEnterpriseMetrics } from \"./infra/rhizohEnterpriseMetrics.js\";\nimport { scoreHealth } from \"./infra/healthScore.js\";\nimport { getTraceById, listTracesBySession } from \"./infra/traceRegistry.js\";\nimport { canonicalEpistemicSealString, hashAndSignEpistemicSeal, EPISTEMIC_SEAL_SCHEMA } from \"./epistemicSeal.js\";\nimport { persistEpistemicLedgerBatch } from \"./epistemicLedgerStore.js\";\nimport { persistEpistemicForecastBatch } from \"./epistemicForecastStore.js\";\nimport { buildRhizohExternalGroundTruthPayload } from \"./rhizohExternalGroundTruthGateway.js\";\nimport { ingestRhizohExternalLossBatchHttp } from \"./rhizohExternalLossIngestGateway.js\";\nimport {\n ingestRhizohProductOutcomeHttp,\n listRhizohProductOutcomeAggregates,\n verifyRhizohOutcomeHmac,\n verifyRhizohOutcomeSourceToken\n} from \"./rhizohProductOutcomeIngestGateway.js\";\nimport { initRhizoh } from \"./rhizohProductionBootstrap.js\";\nimport { handleAcademicObservatoryExportGetV0 } from \"./rhizoh/academicObservatoryHttpV0.js\";\nimport {\n handleLiveSportsBundleGetV0,\n handleLiveNewsHeadlinesGetV0,\n handleLiveWorldFeedGetV0\n} from \"./rhizoh/liveWorldFeedHttpV0.js\";\nimport {\n buildGenesisRuntimeSurfacePayload,\n recordGenesisEpistemicLedgerPersisted,\n recordGenesisEpistemicSealIssued,\n startGenesisCanonicalClock\n} from \"./genesisRuntimeSurfaceV0.js\";\nimport { registerGenesisSseClient, getGenesisContinuitySeq, setGenesisContinuityAfterPublishHook } from \"./genesisContinuityStreamHubV0.js\";\nimport { computeGenesisReplayFingerprintV0 } from \"./genesisReplayFingerprintV0.js\";\nimport { buildGenesisGatewayCapabilitiesV0 } from \"./genesisGatewayCapabilitiesV0.js\";\nimport {\n installGenesisCheckpointSurfaceGetter,\n noteGenesisCheckpointSeqCommitted,\n getLatestGenesisSignedCheckpoint\n} from \"./genesisContinuityCheckpointV0.js\";\nimport { hydrateGenesisContinuityPersistenceBootV0 } from \"./genesisContinuityHydrateBootV0.js\";\nimport { startGenesisContinuityInfraSampler } from \"./genesisContinuityInfraSamplerV0.js\";\nimport {\n genesisCheckpointQueryBySeqV0,\n genesisCheckpointQueryRangeV0,\n genesisCheckpointQueryLineageV0\n} from \"./genesisCheckpointQueryV0.js\";\nimport {\n genesisContinuityEventArchiveEnabled,\n queryGenesisContinuityEventArchiveV0,\n GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION\n} from \"./genesisContinuityEventArchiveV0.js\";\nimport { resolveGenesisReplayRouterV1, GENESIS_REPLAY_ROUTER_SCHEMA } from \"./genesisReplayRouterV1.js\";\nimport {\n computeGenesisReplayTemporalDiffV1,\n GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA\n} from \"./genesisReplayTemporalDiffV1.js\";\nimport { compareGenesisReplayEquivalenceV1 } from \"./genesisReplayEquivalenceV1.js\";\nimport { computeGenesisReplayAnalyticsV1 } from \"./genesisReplayAnalyticsV1.js\";\nimport { computeGenesisReplayEvolutionV1 } from \"./genesisReplayEvolutionV1.js\";\nimport { handleStripeCheckoutCreate, handleStripeMembershipWebhook } from \"./stripeWebhookMembership.js\";\nimport { buildRealityHealthPayload } from \"./edgeRealityConsistencyV1.js\";\nimport {\n assertWalPeerSocketAuthorityV0,\n buildGatewaySubstrateAuthoritySnapshotV0,\n logGatewaySubstrateAuthorityGuardsV0,\n validateWalPeerFeedSignatureV0\n} from \"./gatewaySubstrateAuthorityV0.js\";\nimport {\n buildSubstrateOperationalSnapshotV0,\n recordWalPeerRejectV0,\n renderSubstratePrometheusMetricsV0\n} from \"./infra/substrateOperationalMetrics.js\";\nimport { ingestClientSubstrateHealthV0 } from \"./rhizohSubstrateHealthIngestV0.js\";\nimport { recordSubstrateOtelEventV0 } from \"./infra/opentelemetrySubstrateV0.js\";\nimport {\n GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n getGenesisWorldObservationIngressStatusV0,\n ingestGenesisWorldObservationV0\n} from \"./genesisContinuityClientIngressV0.js\";\nimport {\n getGenesisContinuitySeqAuditV0,\n noteGenesisContinuitySeqAcceptedV0\n} from \"./genesisContinuitySeqGapV0.js\";\n\n/** Render / Fly / Railway: `PORT` — yoksa `CASTLE_GATEWAY_PORT` — yoksa 8090. */\nconst PORT =\n Number(process.env.PORT) ||\n Number(process.env.CASTLE_GATEWAY_PORT) ||\n 8090;\n/** Spiral / world WebSocket server — set after `wss` init; used by genesis runtime surface. */\nlet spiralWssForGenesis = null;\nconst MAX_MESSAGE_BYTES = Number(process.env.CASTLE_MAX_MESSAGE_BYTES || 32 * 1024);\nconst REQUIRED_GATEWAY_TOKEN = process.env.CASTLE_GATEWAY_TOKEN || \"\";\nconst ALLOWED_ORIGINS = parseAllowedOriginsFromEnv().fromList;\nconst httpCors = createHttpCorsPolicy();\n\n/** @returns {string|null} `Access-Control-Allow-Origin`; null = browser CORS fail. */\nfunction accessControlAllowOriginValue(req) {\n return httpCors.accessControlAllowOriginValue(req);\n}\n\nfunction applyHttpCorsHeaders(req, res) {\n return httpCors.applyHttpCorsHeaders(req, res);\n}\nconst REQUIRE_AUTH = process.env.CASTLE_REQUIRE_AUTH === \"true\";\nconst ALLOW_DEV_ANON = process.env.CASTLE_ALLOW_DEV_ANON !== \"false\";\nconst ALLOW_DEV_HTTP_UID = process.env.CASTLE_ALLOW_DEV_HTTP_UID !== \"false\";\n/** Presence mesh: allow `X-Castle-Guest-Id` when Firebase auth absent (default on). Set `false` to require auth only. */\nconst ALLOW_MESH_GUEST = process.env.CASTLE_ALLOW_MESH_GUEST !== \"false\";\nconst SOCIAL_RETRY_MAX = Math.max(0, Math.min(5, Number(process.env.CASTLE_SOCIAL_RETRY_MAX || 2)));\nconst SOCIAL_RETRY_BASE_MS = Math.max(100, Number(process.env.CASTLE_SOCIAL_RETRY_BASE_MS || 700));\nconst TELEGRAM_BOT_TOKEN = process.env.CASTLE_TELEGRAM_BOT_TOKEN || \"\";\nconst WHATSAPP_TOKEN = process.env.CASTLE_WHATSAPP_TOKEN || \"\";\nconst WHATSAPP_PHONE_NUMBER_ID = process.env.CASTLE_WHATSAPP_PHONE_NUMBER_ID || \"\";\nconst RL_RHIZOH_LLM_PER_MIN = Math.max(5, Number(process.env.CASTLE_RL_RHIZOH_LLM_PER_MIN || 40));\nconst RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN || 30)\n);\nconst RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN || 120));\nconst RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN = Math.max(\n 10,\n Number(process.env.CASTLE_RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN || 60)\n);\nconst RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN = Math.max(8, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN || 48));\nconst RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN = Math.max(4, Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN || 24));\nconst RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN || 18)\n);\nconst RL_EPISTEMIC_SEAL_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_EPISTEMIC_SEAL_PER_MIN || 120));\nconst RL_EPISTEMIC_LOG_PER_MIN = Math.max(20, Number(process.env.CASTLE_RL_EPISTEMIC_LOG_PER_MIN || 240));\nconst EPISTEMIC_SEAL_SECRET = String(\n process.env.CASTLE_EPISTEMIC_SEAL_SECRET || process.env.CASTLE_GATEWAY_TOKEN || \"\"\n).trim();\nconst RL_LLM_CONN_TEST_PER_MIN = Math.max(2, Number(process.env.CASTLE_RL_LLM_CONNECTION_TEST_PER_MIN || 8));\n\n/** One-shot production warnings: CORS surface, auth toggle, dev bypass flags (observatory deploy hygiene). */\nfunction logProductionObservatorySurfaceGuardsV0() {\n if (String(process.env.NODE_ENV || \"\").toLowerCase() !== \"production\") return;\n const tag = \"[GATEWAY_PROD_GUARD]\";\n const primaryCors = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n const corsListEmpty = ALLOWED_ORIGINS.length === 0;\n if (corsListEmpty && !primaryCors) {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty and CASTLE_HTTP_CORS_ORIGIN empty — production still merges RHIZOH_BROWSER_ORIGINS_V1 defaults; set env for preview/staging hosts`\n );\n } else if (corsListEmpty && primaryCors && primaryCors !== \"*\") {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty — only CASTLE_HTTP_CORS_ORIGIN set; add web.app + firebaseapp.com (and previews) to CASTLE_ALLOWED_ORIGINS if clients use alternate hostnames`\n );\n }\n if (primaryCors && /localhost|127\\.0\\.0\\.1/i.test(primaryCors)) {\n console.warn(`${tag} CORS: CASTLE_HTTP_CORS_ORIGIN is localhost while NODE_ENV=production`);\n }\n if (REQUIRE_AUTH) {\n console.warn(\n `${tag} AUTH: CASTLE_REQUIRE_AUTH=true — clients must send a valid gateway credential (otherwise silent 401 on replay/SSE); EventSource cannot set custom headers unless proxied`\n );\n if (REQUIRED_GATEWAY_TOKEN.length < 16) {\n console.warn(`${tag} AUTH: CASTLE_GATEWAY_TOKEN missing or short while require-auth enabled`);\n }\n }\n if (ALLOW_DEV_ANON) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_ANON is not \"false\" — dev anonymous path may be live in production (set CASTLE_ALLOW_DEV_ANON=false)`\n );\n }\n if (ALLOW_DEV_HTTP_UID) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_HTTP_UID is not \"false\" — X-Castle-Dev-Uid bypass risk (set CASTLE_ALLOW_DEV_HTTP_UID=false)`\n );\n }\n}\n\nconst rhizohRuntime = initRhizoh();\n\n/** İstemci: llmKeySource veya keySource — env | user_connection | auto */\nfunction normalizeClientLlmKeySource(raw) {\n const s = String(raw ?? \"\").trim().toLowerCase();\n if (s === \"env\" || s === \"server\") return \"env\";\n if (s === \"user_connection\" || s === \"connection\" || s === \"user\") return \"user_connection\";\n if (s === \"auto\" || s === \"\") return \"auto\";\n return null;\n}\n\nfunction jsonReplacerSafeV0(_key, value) {\n if (typeof value === \"bigint\") return Number(value);\n return value;\n}\n\nfunction sendJson(res, statusCode, payload) {\n res.writeHead(statusCode, { \"Content-Type\": \"application/json; charset=utf-8\" });\n try {\n res.end(JSON.stringify(payload, jsonReplacerSafeV0));\n } catch (error) {\n res.end(\n JSON.stringify({\n ok: false,\n error: \"response_serialize_failed\",\n detail: String(error?.message || error)\n })\n );\n }\n}\n\nfunction logRhizohHealth(stage, detail = {}) {\n try {\n console.info(`[CASTLE_${String(stage || \"unknown\")}]`, detail && typeof detail === \"object\" ? detail : {});\n } catch {\n /* noop */\n }\n}\n\nfunction renderInfraMetricsProm() {\n const lines = [\n `castle_gateway_events_processed_total ${infraMetrics.eventsProcessed}`,\n `castle_gateway_duplicate_reject_total ${infraMetrics.duplicateRejects}`,\n `castle_gateway_enqueue_latency_ms ${infraMetrics.enqueueLatencyMs}`,\n `castle_gateway_queue_depth ${infraMetrics.queueDepth}`,\n `castle_gateway_queue_lag_seconds ${(Number(infraMetrics.queueLag || 0) / 1000).toFixed(3)}`,\n `castle_gateway_errors_total ${infraMetrics.errors}`\n ];\n const buckets = infraMetrics.enqueueLatencyBuckets || {};\n for (const k of Object.keys(buckets)) {\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"${k}\"} ${buckets[k]}`);\n }\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"+Inf\"} ${infraMetrics.eventsProcessed}`);\n if (process.env.CASTLE_SUBSTRATE_METRICS !== \"0\") {\n lines.push(renderSubstratePrometheusMetricsV0().trimEnd());\n }\n return lines.join(\"\\n\");\n}\n\nasync function fetchJsonWithTimeout(url, timeoutMs = 1200) {\n const ctl = new AbortController();\n const tid = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const res = await fetch(url, { signal: ctl.signal });\n if (!res.ok) return null;\n return await res.json();\n } catch {\n return null;\n } finally {\n clearTimeout(tid);\n }\n}\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction getHttpPathname(req) {\n let p = String(req.url || \"\").split(\"?\")[0];\n /* Reverse proxies / clients sometimes append `/` — avoid silent 404 on e.g. `/rhizoh/genesis/stream/`. */\n while (p.length > 1 && p.endsWith(\"/\")) {\n p = p.slice(0, -1);\n }\n return p;\n}\n\nfunction rhizohLlmEnvConfigured() {\n const p = String(process.env.CASTLE_LLM_PROVIDER || \"openai\").toLowerCase();\n const key =\n p === \"anthropic\"\n ? process.env.ANTHROPIC_API_KEY\n : p === \"gemini\"\n ? process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY\n : p === \"xai\"\n ? process.env.XAI_API_KEY\n : p === \"deepseek\"\n ? process.env.DEEPSEEK_API_KEY\n : p === \"mistral\"\n ? process.env.MISTRAL_API_KEY\n : p === \"openrouter\"\n ? process.env.OPENROUTER_API_KEY\n : process.env.OPENAI_API_KEY;\n return !!String(key || \"\").trim();\n}\n\n/**\n * @param {import(\"firebase-admin/firestore\").Firestore | null} db\n * @param {number} timeoutMs\n */\nasync function probeFirestoreReachable(db, timeoutMs = 2000) {\n if (!db) return { ok: false, reason: \"no_db\" };\n const timeout = sleep(timeoutMs).then(() => ({ ok: false, reason: \"timeout\" }));\n const work = db\n .collection(\"_castle_gateway_health\")\n .doc(\"ping\")\n .get()\n .then(() => ({ ok: true, reason: \"reachable\" }))\n .catch((e) => ({ ok: false, reason: String(e?.message || e || \"firestore_error\") }));\n return Promise.race([work, timeout]);\n}\n\nfunction stripHtml(text = \"\") {\n return String(text)\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \" \")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \" \")\n .replace(/<[^>]+>/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction haversineMeters(aLat, aLon, bLat, bLon) {\n const R = 6371000;\n const dLat = ((bLat - aLat) * Math.PI) / 180;\n const dLon = ((bLon - aLon) * Math.PI) / 180;\n const sa = Math.sin(dLat / 2);\n const sb = Math.sin(dLon / 2);\n const x = sa * sa + Math.cos((aLat * Math.PI) / 180) * Math.cos((bLat * Math.PI) / 180) * sb * sb;\n return 2 * R * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));\n}\n\nasync function checkPublishTargetHealth(protocol, target) {\n const p = String(protocol || \"\").toUpperCase();\n const t = String(target || \"\").trim();\n if (!t) return { ok: false, reason: \"target_missing\", shouldReconnect: false };\n if (p === \"RTMP\") {\n const ok = t.startsWith(\"rtmp://\") || t.startsWith(\"rtmps://\");\n return { ok, reason: ok ? \"rtmp_target_format_ok\" : \"invalid_rtmp_target\", shouldReconnect: !ok, reconnectInSec: 15 };\n }\n if (p === \"WHIP\" || p === \"SFU\") {\n try {\n const res = await fetch(t, { method: \"OPTIONS\" });\n const ok = res.status < 500;\n return { ok, reason: `http_${res.status}`, shouldReconnect: !ok, reconnectInSec: 8 };\n } catch {\n return { ok: false, reason: \"network_unreachable\", shouldReconnect: true, reconnectInSec: 8 };\n }\n }\n return { ok: true, reason: \"unchecked_protocol\", shouldReconnect: false };\n}\n\nasync function fetchTextFromUrl(url) {\n const res = await fetch(url, { method: \"GET\" });\n if (!res.ok) throw new Error(`fetch_${res.status}`);\n const ct = String(res.headers.get(\"content-type\") || \"\");\n if (ct.includes(\"application/pdf\")) throw new Error(\"pdf_binary_not_supported_directly\");\n const body = await res.text();\n return stripHtml(body).slice(0, 14000);\n}\n\nfunction buildTaskActionGraph(task = \"\", context = {}) {\n const t = String(task || \"\").toLowerCase();\n if (t.includes(\"dinner\") || t.includes(\"akşam yemeği\") || t.includes(\"aksam yemegi\")) {\n return {\n taskTitle: \"Dinner Prep Planner\",\n nodes: [\n { id: \"n1\", title: \"Kitchen preheat\", action: \"smart-oven.preheat\", params: { temperatureC: 190, mode: \"fan\" } },\n { id: \"n2\", title: \"Lighting comfort\", action: \"smart-light.set_scene\", params: { scene: \"warm_dinner\" } },\n { id: \"n3\", title: \"Fridge inventory check\", action: \"smart-fridge.inventory_scan\", params: {} },\n { id: \"n4\", title: \"Reminder notification\", action: \"smart-watch.notify\", params: { text: \"Dinner prep started.\" } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" },\n { from: \"n3\", to: \"n4\" }\n ]\n };\n }\n return {\n taskTitle: task || \"General Planner\",\n nodes: [\n { id: \"n1\", title: \"Safety check\", action: \"device.safety_check\", params: { context } },\n { id: \"n2\", title: \"Primary action\", action: \"device.execute_task\", params: { task } },\n { id: \"n3\", title: \"Report\", action: \"studio.timeline_note\", params: { task } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" }\n ]\n };\n}\n\nfunction buildEthicsPrograms() {\n return {\n policyVersion: \"v1\",\n tracks: [\n {\n id: \"elderly-care-assist\",\n audience: \"elderly\",\n title: \"Elderly Assistance Track\",\n principles: [\"human-in-the-loop\", \"consent-first\", \"low-friction alerts\"],\n modules: [\"medication-reminder\", \"fall-risk-check\", \"family-notify\", \"daily-companion-brief\"]\n },\n {\n id: \"kids-safe-learning\",\n audience: \"children\",\n title: \"Kids Safe Learning Track\",\n principles: [\"guardian-consent\", \"age-appropriate-language\", \"content-filtering\"],\n modules: [\"study-coach\", \"screen-time-balance\", \"safe-social-share\", \"emergency-contact\"]\n }\n ]\n };\n}\n\nasync function deliverViaTelegram(channel, text) {\n const chatId = String(channel.endpoint || \"\").trim();\n if (!TELEGRAM_BOT_TOKEN) throw new Error(\"telegram_token_missing\");\n if (!chatId) throw new Error(\"telegram_chat_id_missing\");\n const url = `https://api.telegram.org/bot${encodeURIComponent(TELEGRAM_BOT_TOKEN)}/sendMessage`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ chat_id: chatId, text })\n });\n if (!res.ok) throw new Error(`telegram_http_${res.status}`);\n return { provider: \"telegram\", status: res.status };\n}\n\nasync function deliverViaWhatsapp(channel, text) {\n const to = String(channel.endpoint || \"\").trim();\n if (!WHATSAPP_TOKEN) throw new Error(\"whatsapp_token_missing\");\n if (!WHATSAPP_PHONE_NUMBER_ID) throw new Error(\"whatsapp_phone_number_id_missing\");\n if (!to) throw new Error(\"whatsapp_to_missing\");\n const url = `https://graph.facebook.com/v22.0/${encodeURIComponent(WHATSAPP_PHONE_NUMBER_ID)}/messages`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${WHATSAPP_TOKEN}` },\n body: JSON.stringify({ messaging_product: \"whatsapp\", to, type: \"text\", text: { body: text } })\n });\n if (!res.ok) throw new Error(`whatsapp_http_${res.status}`);\n return { provider: \"whatsapp\", status: res.status };\n}\n\nasync function deliverViaWebhook(channel, text) {\n const endpoint = String(channel.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"webhook_endpoint_missing\");\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text, platform: channel.platform || \"webhook\", ts: Date.now() })\n });\n if (!res.ok) throw new Error(`webhook_http_${res.status}`);\n return { provider: \"webhook\", status: res.status };\n}\n\nasync function deliverSocialMessage(channel, text) {\n const platform = String(channel.platform || \"webhook\").toLowerCase();\n if (platform === \"telegram\") return deliverViaTelegram(channel, text);\n if (platform === \"whatsapp\") return deliverViaWhatsapp(channel, text);\n return deliverViaWebhook(channel, text);\n}\n\nasync function deliverSocialWithRetry(channel, text) {\n let attempt = 0;\n let lastError = null;\n while (attempt <= SOCIAL_RETRY_MAX) {\n try {\n const result = await deliverSocialMessage(channel, text);\n return { ok: true, attempt: attempt + 1, ...result };\n } catch (error) {\n lastError = error;\n if (attempt >= SOCIAL_RETRY_MAX) break;\n const jitter = Math.floor(Math.random() * 90);\n await sleep(SOCIAL_RETRY_BASE_MS * Math.pow(2, attempt) + jitter);\n }\n attempt += 1;\n }\n return { ok: false, attempt: attempt + 1, error: String(lastError?.message || \"delivery_failed\") };\n}\n\nasync function dispatchCommandToAdapter(device, command) {\n const adapter = String(command?.adapter || device?.adapter || \"websocket\").toLowerCase();\n const endpoint = String(device?.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"device_endpoint_missing\");\n if (adapter === \"mqtt\") {\n // MQTT delivery is proxied via a bridge endpoint (HTTP webhook style).\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n type: \"mqtt-command\",\n topic: `castle/devices/${device.id}/commands`,\n action: command.action,\n params: command.params || {}\n })\n });\n if (!res.ok) throw new Error(`mqtt_bridge_http_${res.status}`);\n return { ok: true, adapter, via: \"http-bridge\", status: res.status };\n }\n if (!endpoint.startsWith(\"ws://\") && !endpoint.startsWith(\"wss://\")) throw new Error(\"invalid_websocket_endpoint\");\n const sendResult = await new Promise((resolve, reject) => {\n const ws = new WebSocket(endpoint);\n const timer = setTimeout(() => {\n try {\n ws.close();\n } catch {\n /* noop */\n }\n reject(new Error(\"websocket_timeout\"));\n }, 3000);\n ws.on(\"open\", () => {\n ws.send(\n JSON.stringify({\n type: \"castle-command\",\n commandId: command.id,\n deviceId: command.deviceId,\n action: command.action,\n params: command.params || {}\n }),\n (err) => {\n clearTimeout(timer);\n ws.close();\n if (err) reject(new Error(\"websocket_send_failed\"));\n else resolve({ ok: true, adapter: \"websocket\", via: \"ws\" });\n }\n );\n });\n ws.on(\"error\", () => {\n clearTimeout(timer);\n reject(new Error(\"websocket_connect_failed\"));\n });\n });\n return sendResult;\n}\n\nfunction readHttpJson(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => {\n if (!buf) return resolve({});\n const payload = safeJsonParse(buf);\n if (!payload || typeof payload !== \"object\") return reject(new Error(\"invalid_json_payload\"));\n resolve(payload);\n });\n req.on(\"error\", reject);\n });\n}\n\n/** Ham gövde (HMAC doğrulaması için). */\nfunction readHttpBodyText(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => resolve(buf));\n req.on(\"error\", reject);\n });\n}\n\nfunction maskOpaqueId(value) {\n const t = String(value || \"\");\n if (!t) return \"(empty)\";\n if (t.length <= 8) return \"****\";\n return `${t.slice(0, 4)}…${t.slice(-2)}`;\n}\n\nfunction readGatewayHttpToken(req) {\n const x = String(req.headers?.[\"x-castle-gateway-token\"] || \"\").trim();\n if (x) return x;\n const auth = String(req.headers?.authorization || \"\");\n const m = /^Bearer\\s+(.+)$/i.exec(auth);\n return m ? String(m[1] || \"\").trim() : \"\";\n}\n\nasync function resolveHttpUser(req) {\n const authResult = await verifyClientToken(req);\n if (authResult?.ok && authResult.user?.uid) {\n return { ok: true, uid: String(authResult.user.uid), mode: authResult.kind || \"auth\" };\n }\n if (ALLOW_DEV_ANON && ALLOW_DEV_HTTP_UID) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) return { ok: true, uid: `dev-${devUid.slice(0, 64)}`, mode: \"dev-header\" };\n }\n return { ok: false, reason: authResult?.reason || \"auth_required\" };\n}\n\n/**\n * Epistemic ledger ingest — Firebase uid OR valid transport token + X-Castle-Dev-Uid.\n * Prod disables CASTLE_ALLOW_DEV_HTTP_UID; LLM may still work while telemetry 401'd without this.\n */\nasync function resolveEpistemicIngestActor(req) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok && tok === REQUIRED_GATEWAY_TOKEN) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) {\n return { ok: true, uid: `gw-telemetry-${devUid.slice(0, 64)}`, mode: \"gateway-token-dev-uid\" };\n }\n return { ok: false, reason: \"dev_uid_required\" };\n }\n if (tok) {\n return { ok: false, reason: \"gateway_token_mismatch\" };\n }\n }\n const auth = await resolveHttpUser(req);\n if (auth.ok) return auth;\n return { ok: false, reason: auth.reason || \"auth_required\" };\n}\n\n/** Mesh actor: Firebase uid, dev header, or `guest:<uuid>` via header / `guestId` query (SSE cannot set headers). */\nasync function resolveMeshActor(req) {\n const auth = await resolveHttpUser(req);\n if (auth.ok) return { ok: true, clientUid: auth.uid, mode: auth.mode };\n if (ALLOW_MESH_GUEST) {\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const gid = String(req.headers?.[\"x-castle-guest-id\"] || q.searchParams.get(\"guestId\") || \"\").trim();\n if (gid) {\n const normalized = gid.startsWith(\"guest:\") ? gid.slice(0, 180) : `guest:${gid.slice(0, 160)}`;\n return { ok: true, clientUid: normalized, mode: \"guest-header\" };\n }\n }\n return { ok: false, reason: \"mesh_auth_required\" };\n}\n\n/** Live genesis runtime JSON (GET /rhizoh/genesis/runtime + SSE infra sampler). */\nasync function buildGenesisRuntimeSurfacePayloadLive() {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n let spiralWsActive = 0;\n if (spiralWssForGenesis) {\n for (const c of spiralWssForGenesis.clients) {\n if (c.readyState === 1) spiralWsActive += 1;\n }\n }\n const base = buildGenesisRuntimeSurfacePayload({\n infraMetrics,\n scoredHealth: scored,\n rhizohEnterpriseMetrics,\n mesh: meshContinuityAggregate(),\n workerHealth: worker,\n port: PORT,\n spiralWebSocketClientsActive: spiralWsActive\n });\n return {\n ...base,\n replayFingerprint: computeGenesisReplayFingerprintV0(base),\n gatewayCapabilities: buildGenesisGatewayCapabilitiesV0(),\n genesisStream: {\n schema: \"castle.genesis.stream_cursor.v0\",\n lastAcceptedSeq: getGenesisContinuitySeq(),\n clientIngress: {\n schema: GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n contract: \"castle.world_observation.ingress_envelope.v1\",\n postPath: rhizohRuntime.routes.genesisIngress,\n closure: getGenesisWorldObservationIngressStatusV0()\n },\n seqContinuity: getGenesisContinuitySeqAuditV0(),\n eventArchive: genesisContinuityEventArchiveEnabled()\n ? {\n enabled: true,\n projection: GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION,\n queryPath: rhizohRuntime.routes.genesisContinuityEvents\n }\n : { enabled: false },\n replayRouter: {\n schema: GENESIS_REPLAY_ROUTER_SCHEMA,\n queryPath: rhizohRuntime.routes.genesisReplay,\n queryHint: \"from=&to= or range=from-to; optional type=&checkpoints=0\",\n temporalDiffPath: rhizohRuntime.routes.genesisReplayDiff,\n temporalDiffSchema: GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA,\n equivalencePath: rhizohRuntime.routes.genesisReplayEquivalence,\n analyticsPath: rhizohRuntime.routes.genesisReplayAnalytics,\n evolutionPath: rhizohRuntime.routes.genesisReplayEvolution\n }\n }\n };\n}\n\nconst httpServer = createServer(async (req, res) => {\n const corsAllow = applyHttpCorsHeaders(req, res);\n if (req.method === \"OPTIONS\") {\n if (!corsAllow && req.headers?.origin) {\n console.warn(\"[GATEWAY_CORS] preflight rejected for origin:\", req.headers.origin);\n }\n res.writeHead(corsAllow ? 204 : 403);\n res.end();\n return;\n }\n\n const pathname = getHttpPathname(req);\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/genesis/__ping\") {\n sendJson(res, 200, {\n ok: true,\n pid: process.pid,\n entry: __filename,\n time: Date.now(),\n genesis: true\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/epistemic/__probe\") {\n sendJson(res, 200, {\n ok: true,\n epistemicIngestActorV2: true,\n routes: {\n seal: rhizohRuntime.routes.epistemicSeal,\n logsBatch: rhizohRuntime.routes.epistemicLogsBatch\n },\n hasGatewayToken: Boolean(REQUIRED_GATEWAY_TOKEN),\n gatewayTokenLen: REQUIRED_GATEWAY_TOKEN ? REQUIRED_GATEWAY_TOKEN.length : 0\n });\n return;\n }\n\n if (\n req.method === \"GET\" &&\n (pathname === \"/rhizoh/live/world-feed\" ||\n pathname === \"/rhizoh/live/sports-bundle\" ||\n pathname === \"/rhizoh/live/news-headlines\")\n ) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_live_world_feed:${ip}`, RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const out =\n pathname === \"/rhizoh/live/sports-bundle\"\n ? await handleLiveSportsBundleGetV0()\n : pathname === \"/rhizoh/live/news-headlines\"\n ? await handleLiveNewsHeadlinesGetV0(req)\n : await handleLiveWorldFeedGetV0(req);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/webhooks/stripe\") {\n await handleStripeMembershipWebhook(req, res);\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/billing/stripe/checkout\") {\n await handleStripeCheckoutCreate(req, res, { readHttpJson, resolveHttpUser, sendJson });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/live\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, live: false, dns: true, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n sendJson(res, 200, { ok: true, live: true, dns: true, service: \"castle-gateway\", ts: Date.now() });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/cors\") {\n const reqOrigin = normalizeHttpOrigin(req.headers?.origin);\n const allowOrigin = accessControlAllowOriginValue(req);\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n requestOrigin: reqOrigin || null,\n allowed: Boolean(allowOrigin),\n allowOrigin: allowOrigin || null,\n allowSetSize: httpCors.allowSet.size\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/ready\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, ready: false, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n try {\n const persistence = getFirebasePersistence();\n const llmProbe = rhizohLlmEnvConfigured();\n sendJson(res, 200, {\n ok: true,\n ready: true,\n dns: true,\n persistence: persistence.mode,\n service: \"castle-gateway\",\n gateway: \"ok\",\n llm: llmProbe ? \"ok\" : \"degraded\",\n spine: llmProbe ? \"ok\" : \"partial\"\n });\n } catch (e) {\n sendJson(res, 503, { ok: false, ready: false, reason: String(e?.message || e), service: \"castle-gateway\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/reality\") {\n const payload = buildRealityHealthPayload();\n sendJson(res, payload.ok ? 200 : 503, payload);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/substrate\") {\n const authority = buildGatewaySubstrateAuthoritySnapshotV0();\n const operational = buildSubstrateOperationalSnapshotV0();\n const drift = buildRealityHealthPayload();\n const ok = Boolean(authority.productionAuthoritySatisfied) && drift.ok;\n sendJson(res, ok ? 200 : 503, {\n ok,\n service: \"castle-gateway\",\n ts: Date.now(),\n authority,\n operational,\n drift: { ok: drift.ok, errors: drift.drift?.errors, warnings: drift.drift?.warnings }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/substrate/health\") {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const body = await readHttpJson(req);\n const uid = (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestClientSubstrateHealthV0(body, { uid, clientId: uid });\n if (!result.ok) {\n sendJson(res, 400, result);\n return;\n }\n recordSubstrateOtelEventV0(\"client_health_ingest\", { uid: String(uid).slice(0, 32) });\n sendJson(res, 200, result);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/deps\") {\n const t0 = Date.now();\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, {\n ok: false,\n dns: true,\n llm: false,\n firestore: false,\n phase: \"maintenance\",\n latencyMs: Date.now() - t0,\n service: \"castle-gateway\"\n });\n return;\n }\n const persistence = getFirebasePersistence();\n let firestoreOk = persistence.mode !== \"firebase\";\n let firestoreDetail = persistence.mode === \"file\" ? \"file_mode\" : \"not_applicable\";\n if (persistence.mode === \"firebase\" && persistence.db) {\n const pr = await probeFirestoreReachable(persistence.db, 2000);\n firestoreOk = pr.ok;\n firestoreDetail = pr.reason || (pr.ok ? \"reachable\" : \"fail\");\n } else if (persistence.mode === \"firebase\" && !persistence.db) {\n firestoreOk = false;\n firestoreDetail = \"firebase_not_initialized\";\n }\n const llmOk = rhizohLlmEnvConfigured();\n const overallOk = llmOk && firestoreOk;\n const latencyMs = Date.now() - t0;\n sendJson(res, 200, {\n ok: overallOk,\n dns: true,\n llm: llmOk,\n firestore: persistence.mode === \"firebase\" ? firestoreOk : false,\n persistence: persistence.mode,\n firestoreDetail,\n latencyMs,\n service: \"castle-gateway\",\n wsPort: PORT\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health\") {\n const persistence = getFirebasePersistence();\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n wsPort: PORT,\n persistence: persistence.mode,\n presenceMesh: \"sse\"\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/health\") {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n sendJson(res, 200, {\n ok: true,\n role: \"gateway\",\n queueLagMs: infraMetrics.queueLag,\n queueDepth: infraMetrics.queueDepth,\n errors: infraMetrics.errors,\n status: scored.status,\n score: scored.score,\n reasons: scored.reasons,\n worker: worker\n ? {\n status: worker.status,\n score: worker.score,\n reasons: worker.reasons\n }\n : null,\n timestamp: Date.now()\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisRuntime) {\n const payload = await buildGenesisRuntimeSurfacePayloadLive();\n sendJson(res, 200, payload);\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.genesisIngress) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const actor = await resolveMeshActor(req);\n const clientId = actor.ok ? actor.clientUid : (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestGenesisWorldObservationV0(body, { clientId });\n if (!result.ok) {\n const status =\n result.error === \"rate_limited\" || result.error === \"agent_spoke_throttled\"\n ? 429\n : result.error === \"payload_too_large\"\n ? 413\n : 400;\n sendJson(res, status, result);\n return;\n }\n sendJson(res, result.idempotent ? 200 : 202, result);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisStream) {\n const corsOrigin = accessControlAllowOriginValue(req);\n if (!corsOrigin && String(process.env.NODE_ENV || \"\").toLowerCase() === \"production\") {\n const og = String(req.headers?.origin || \"\").trim();\n if (og) {\n console.warn(\"[GATEWAY] genesis SSE: missing Access-Control-Allow-Origin for browser Origin (CORS whitelist mismatch):\", og);\n }\n }\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n res.write(\"retry: 3000\\n\\n\");\n res.write(\": genesis continuity stream\\n\\n\");\n res.write(\"event: genesis\\n\");\n res.write(`data: {\"booted\":true,\"pid\":${process.pid}}\\n\\n`);\n registerGenesisSseClient(res, req);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLatest) {\n const cp = getLatestGenesisSignedCheckpoint();\n if (!cp) {\n sendJson(res, 404, { ok: false, error: \"no_checkpoint_yet\", hint: \"wait_for_seq_interval_or_configure_signing_secret\" });\n return;\n }\n sendJson(res, 200, { ok: true, checkpoint: cp });\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(rhizohRuntime.routes.genesisCheckpointBySeqPrefix)) {\n const tail = pathname.slice(rhizohRuntime.routes.genesisCheckpointBySeqPrefix.length).split(\"/\")[0];\n const seq = parseInt(String(tail || \"\").trim(), 10);\n const out = await genesisCheckpointQueryBySeqV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"checkpoint_not_found\") {\n sendJson(res, 404, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointRange) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryRangeV0(from, to);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"range_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLineage) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const seq = parseInt(String(u.searchParams.get(\"seq\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryLineageV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"lineage_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisContinuityEvents) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const limit = parseInt(String(u.searchParams.get(\"limit\") || \"\").trim(), 10);\n const out = await queryGenesisContinuityEventArchiveV0(from, to, type, limit);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\" || out.error === \"event_archive_disabled\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"archive_file_too_large\") {\n sendJson(res, 413, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplay) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await resolveGenesisReplayRouterV1({\n from: fromSeq,\n to: toSeq,\n type,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayDiff) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const out = await computeGenesisReplayTemporalDiffV1({ from: fromSeq, to: toSeq, type });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEquivalence) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from1 = parseInt(String(u.searchParams.get(\"from1\") || \"\").trim(), 10);\n const to1 = parseInt(String(u.searchParams.get(\"to1\") || \"\").trim(), 10);\n const from2 = parseInt(String(u.searchParams.get(\"from2\") || \"\").trim(), 10);\n const to2 = parseInt(String(u.searchParams.get(\"to2\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await compareGenesisReplayEquivalenceV1({\n from1,\n to1,\n from2,\n to2,\n type,\n includeCheckpoints\n });\n if (\n !out.ok &&\n (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")\n ) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayAnalytics) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayAnalyticsV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEvolution) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const collapseWindows = parseInt(String(u.searchParams.get(\"collapseWindows\") || \"0\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayEvolutionV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n collapseWindows,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/metrics\") {\n res.writeHead(200, { \"Content-Type\": \"text/plain; version=0.0.4; charset=utf-8\" });\n res.end(renderInfraMetricsProm());\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.infraPrometheusMetrics) {\n if (process.env.CASTLE_PROMETHEUS_METRICS !== \"1\") {\n sendJson(res, 404, { ok: false, error: \"prometheus_metrics_disabled\" });\n return;\n }\n res.writeHead(200, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(renderRhizohPrometheusMetrics());\n return;\n }\n if (req.method === \"GET\" && pathname.startsWith(\"/infra/traces/\")) {\n const traceId = decodeURIComponent(pathname.slice(\"/infra/traces/\".length));\n const item = await getTraceById(traceId);\n if (!item) return sendJson(res, 404, { ok: false, error: \"trace_not_found\" });\n return sendJson(res, 200, { ok: true, trace: item });\n }\n if (req.method === \"GET\" && pathname === \"/infra/traces\") {\n const u = new URL(String(req.url || \"/infra/traces\"), `http://${req.headers.host || \"localhost\"}`);\n const sessionId = String(u.searchParams.get(\"sessionId\") || \"\").trim();\n const items = sessionId ? await listTracesBySession(sessionId) : [];\n return sendJson(res, 200, { ok: true, sessionId, traces: items });\n }\n\n /** Presence room mesh (C): JOIN / LEAVE / SNAPSHOT / DELTA / REPLAY (HTTP); SUBSCRIBE = SSE stream. */\n if (req.method === \"POST\" && pathname === \"/presence/mesh/join\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid, clientUid: actor.clientUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/leave\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshLeave(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/snapshot\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n sendJson(res, 200, meshSnapshot(roomUid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/delta\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, MAX_MESSAGE_BYTES);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const seq = meshAppendDelta(roomUid, actor.clientUid, {\n node: body?.node,\n projectionPatch: body?.projectionPatch,\n writerSubject: body?.writerSubject\n });\n sendJson(res, 200, { ok: true, roomUid, seq });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/replay\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const fromNodeId = body?.fromNodeId != null ? String(body.fromNodeId) : \"\";\n const fromSeq = body?.fromSeq;\n if (fromNodeId) {\n sendJson(res, 200, meshReplayFromNodeId(roomUid, fromNodeId));\n return;\n }\n sendJson(res, 200, meshReplayFromSeq(roomUid, fromSeq));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/presence/mesh/subscribe\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const roomUid = String(q.searchParams.get(\"roomUid\") || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n const write = (chunk) => {\n try {\n res.write(chunk);\n } catch {\n /* closed */\n }\n };\n const unsub = meshSubscribeSse(roomUid, write);\n const onClose = () => {\n unsub();\n meshLeave(roomUid, actor.clientUid);\n try {\n res.end();\n } catch {\n /* ignore */\n }\n };\n req.on(\"close\", onClose);\n req.on(\"aborted\", onClose);\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, uid: auth.uid, mode: auth.mode, items: await listConnections(auth.uid) });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const id = await createConnection(auth.uid, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"create_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n await updateConnection(auth.uid, id, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"update_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"DELETE\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n await deleteConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"delete_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections/test\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n if (!checkHttpRateLimit(`llm_conn_test:${auth.uid}`, RL_LLM_CONN_TEST_PER_MIN, 60_000)) {\n return sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n }\n const hasBodyKey = !!String(payload?.apiKey || \"\").trim();\n const result = await queryRhizohLlm(\n {\n message: String(payload?.message || \"Ping from Castle Gateway\"),\n context: payload?.context || { source: \"connection_test\" },\n provider: payload?.provider,\n model: payload?.model,\n apiKey: payload?.apiKey || \"\"\n },\n { allowExternalApiKey: true, llmKeySource: hasBodyKey ? \"auto\" : \"env\" }\n );\n logLlmAccess({\n route: \"/llm/connections/test\",\n uid: auth.uid,\n llmKeyBillingOwner: result.llmKeyBillingOwner,\n llmKeyOrigin: result.llmKeyOrigin,\n provider: result.provider,\n model: result.model,\n connectionId: null\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"connection_test_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAgentIdentities(auth.uid);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerAgentIdentity({\n ownerUid: auth.uid,\n agentId: payload?.agentId,\n role: payload?.role,\n personaSeed: payload?.personaSeed || {},\n capabilityLevel: payload?.capabilityLevel\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/agents/identities/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const owns = await getAgentIdentity(id);\n if (!owns || owns.ownerUid !== auth.uid) return sendJson(res, 403, { ok: false, error: \"forbidden\" });\n const payload = await readHttpJson(req);\n const row = await updateAgentIdentity(id, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/academy/events\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/academy/events\"}`);\n const status = String(url.searchParams.get(\"status\") || \"\");\n const items = await listAcademyEvents({ ownerUid: auth.uid, status, limit: 120 });\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/academy/events\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const ev = await queueAcademyEvent({\n type: payload?.type,\n roomId: payload?.roomId,\n ownerUid: auth.uid,\n participants: payload?.participants || [],\n topic: payload?.topic || \"academy\",\n payload: payload?.payload || {}\n });\n sendJson(res, 200, { ok: true, event: ev });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && String(req.url || \"\").startsWith(\"/academy/events/\") && String(req.url || \"\").endsWith(\"/resolve\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const parts = String(req.url || \"\").split(\"/\").filter(Boolean);\n const eventId = parts[2] || \"\";\n try {\n const payload = await readHttpJson(req);\n const out = await resolveAcademyEvent(eventId, payload || {});\n sendJson(res, 200, { ok: true, event: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_resolve_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/memory\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/memory\"}`);\n const scope = url.searchParams.get(\"scope\") === \"agents\" ? \"agents\" : \"users\";\n const id = String(url.searchParams.get(\"id\") || (scope === \"agents\" ? \"\" : auth.uid));\n const kind = String(url.searchParams.get(\"kind\") || \"\");\n const limit = Number(url.searchParams.get(\"limit\") || 40);\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n sendJson(res, 200, { ok: true, scope, id, kind, items: await listMemories({ scope, id, kind, limit }) });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const profile = await getPersonaGoalMemory(auth.uid);\n sendJson(res, 200, { ok: true, profile });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const profile = await setPersonaGoalMemory({\n uid: auth.uid,\n personaPatch: payload?.personaPatch || {},\n goals: Array.isArray(payload?.goals) ? payload.goals : null,\n preferencesPatch: payload?.preferencesPatch || {}\n });\n sendJson(res, 200, { ok: true, profile });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"profile_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/compact\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const out = await autoCompactMemories({ scope, id });\n sendJson(res, 200, { ok: true, scope, id, ...out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_compact_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/context\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const context = await getMemoryContext({\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n query: String(payload?.query || \"\"),\n limit: Number(payload?.limit || 80)\n });\n sendJson(res, 200, { ok: true, context });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_context_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/place-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const placeName = String(payload?.placeName || \"\").trim();\n if (!placeName) return sendJson(res, 400, { ok: false, error: \"placeName_required\" });\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const persona = String(payload?.waypointPersona || payload?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(placeName)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Place: ${placeName}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Give a concise conversational briefing about this place for a walking-route companion.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-place-brief\", placeName, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `PLACE_BRIEF:${placeName} => ${String(summary.reply || \"\").slice(0, 900)}`,\n tags: [\"place\", \"brief\", \"event-layer\"],\n importance: 0.62,\n meta: { placeName }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"place-brief\",\n text: `${placeName} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: {\n placeName,\n directive: summary.directive,\n lat: hit?.lat ? Number(hit.lat) : null,\n lon: hit?.lon ? Number(hit.lon) : null\n }\n });\n sendJson(res, 200, { ok: true, place: hit || null, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"place_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/route-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const waypoints = Array.isArray(payload?.waypoints) ? payload.waypoints.slice(0, 24) : [];\n const out = [];\n for (const wp of waypoints) {\n const name = String(wp?.name || \"\").trim();\n if (!name) continue;\n const persona = String(wp?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(name)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Waypoint: ${name}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Generate a concise spoken briefing for route waypoint.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-route-brief\", waypoint: name, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n out.push({\n name,\n persona,\n place: hit || null,\n reply: summary.reply,\n directive: summary.directive\n });\n }\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `ROUTE_BRIEF: waypoints=${out.length} ${out.map((x) => `${x.name}:${x.persona}`).join(\" | \")}`.slice(0, 1300),\n tags: [\"route\", \"waypoint\", \"brief\", \"event-layer\"],\n importance: 0.74,\n meta: { count: out.length }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"route-brief\",\n text: out.map((x) => `${x.name}:${String(x.reply || \"\").slice(0, 140)}`).join(\" | \").slice(0, 1400),\n roomId: \"studio-main\",\n meta: {\n waypointCount: out.length,\n waypoints: out.map((x) => ({\n name: x.name,\n persona: x.persona,\n lat: x.place?.lat ? Number(x.place.lat) : null,\n lon: x.place?.lon ? Number(x.place.lon) : null\n }))\n }\n });\n sendJson(res, 200, { ok: true, items: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"route_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/pdf-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const title = String(payload?.title || \"document\");\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const providedText = String(payload?.text || \"\");\n const url = String(payload?.url || \"\");\n let content = providedText;\n if (!content && url) content = await fetchTextFromUrl(url);\n if (!content) return sendJson(res, 400, { ok: false, error: \"text_or_supported_url_required\" });\n const summary = await queryRhizohLlm(\n {\n message: `Summarize this document and provide key points + practical actions.\\nTitle:${title}\\nContent:\\n${content.slice(0, 12000)}`,\n context: { source: \"event-layer-pdf-brief\", title },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `DOC_BRIEF:${title} => ${String(summary.reply || \"\").slice(0, 1000)}`,\n tags: [\"document\", \"brief\", \"library\", \"event-layer\"],\n importance: 0.7,\n meta: { title, url: url || null }\n });\n await addTranscript(auth.uid, {\n source: \"library-brief\",\n eventType: \"pdf-brief\",\n text: `${title} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { title }\n });\n sendJson(res, 200, { ok: true, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/pdf/upload\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req, 8 * 1024 * 1024);\n const job = await enqueuePdfJob({\n uid: auth.uid,\n fileName: payload?.fileName || \"document.pdf\",\n bytesBase64: payload?.contentBase64\n });\n sendJson(res, 200, { ok: true, job });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_upload_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/session\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createPublishSession(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session\",\n text: `Publish session armed: ${row.protocol} ${row.target}`.slice(0, 500),\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: row.id, bridge: row.bridge }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/studio/publish/sessions\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listPublishSessions(auth.uid, 80);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/health-check\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const protocol = String(payload?.protocol || \"WHIP\");\n const target = String(payload?.target || \"\");\n const health = await checkPublishTargetHealth(protocol, target);\n if (payload?.sessionId) {\n await updatePublishSession(auth.uid, String(payload.sessionId), {\n status: health.ok ? \"healthy\" : \"degraded\",\n reconnectPolicy: {\n enabled: true,\n backoffSec: [2, 5, 10, 20],\n nextRetryInSec: health.reconnectInSec || 0,\n maxRetries: 8,\n reason: health.reason\n }\n });\n }\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-health\",\n text: `${protocol} ${target} => ${health.ok ? \"healthy\" : \"degraded\"} (${health.reason})`,\n roomId: \"studio-main\",\n meta: { protocol, target, reconnect: !!health.shouldReconnect }\n });\n sendJson(res, 200, { ok: true, health });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_health_check_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/studio/publish/sessions/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updatePublishSession(auth.uid, id, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session-update\",\n text: `Session ${id} -> ${row.status || \"updated\"}`,\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: id, status: row.status, ingestUrl: row.ingestUrl || \"\" }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/transcripts\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/transcripts\"}`);\n const limit = Math.max(1, Math.min(200, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await listTranscripts(auth.uid, limit);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/transcripts\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await addTranscript(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"transcript_write_failed\" });\n }\n return;\n }\n\n /** E1: broadcast intent → GreenRoom capability → transcript + memory artifact */\n if (req.method === \"POST\" && req.url === \"/studio/capabilities/greenroom\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const traceId = String(payload?.traceId || `TRC-${Date.now().toString(36).toUpperCase()}`);\n const title = String(payload?.title || \"Castle Live Broadcast\").slice(0, 200);\n const intentRaw = String(payload?.intentRaw || \"\").slice(0, 800);\n const audienceEstimate = Math.max(0, Math.min(50_000_000, Number(payload?.audienceEstimate) || 1200));\n const roomId = String(payload?.roomId || \"greenroom-main\");\n const transcriptRow = await addTranscript(auth.uid, {\n source: \"rhizoh\",\n eventType: \"BROADCAST_ROUTED\",\n text: `GreenRoom · ${title}${intentRaw ? `\\nIntent: ${intentRaw}` : \"\"}`,\n roomId,\n meta: {\n ack: \"BROADCAST_ROUTED\",\n capability: \"greenroom\",\n intentType: \"broadcast\",\n traceId,\n audienceEstimate,\n artifact: {\n kind: \"broadcast_card\",\n title,\n traceId,\n roomId,\n createdAt: Date.now()\n }\n }\n });\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n text: `GREENROOM_BROADCAST:${title} trace=${traceId}`,\n tags: [\"broadcast\", \"greenroom\", \"artifact\", \"castle-library\"],\n importance: 0.72,\n kind: \"episodic\",\n meta: { traceId, capability: \"greenroom\", transcriptId: transcriptRow.id }\n });\n sendJson(res, 200, {\n ok: true,\n ack: \"BROADCAST_ROUTED\",\n traceId,\n transcript: transcriptRow,\n replayPath: `/replay/${encodeURIComponent(traceId)}`,\n sharePath: `/replay/${encodeURIComponent(traceId)}`\n });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"greenroom_capability_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/metadata/search\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/metadata/search\"}`);\n const q = String(url.searchParams.get(\"q\") || \"\");\n const limit = Math.max(1, Math.min(300, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await searchMetadata(auth.uid, q, limit);\n sendJson(res, 200, { ok: true, q, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerDevice(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, device: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"device_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const lat = Number(payload?.lat);\n const lon = Number(payload?.lon);\n const speed = Math.max(0, Number(payload?.speed || 0));\n const cfgCenterLat = Number(process.env.CASTLE_GEOFENCE_CENTER_LAT || 41.0082);\n const cfgCenterLon = Number(process.env.CASTLE_GEOFENCE_CENTER_LON || 28.9784);\n const cfgRadiusM = Math.max(100, Number(process.env.CASTLE_GEOFENCE_RADIUS_M || 55000));\n const cfgSpeedCap = Math.max(1, Number(process.env.CASTLE_SPEED_CAP_MPS || 35));\n const dist = Number.isFinite(lat) && Number.isFinite(lon) ? haversineMeters(cfgCenterLat, cfgCenterLon, lat, lon) : 0;\n const geofenceOk = !Number.isFinite(dist) ? true : dist <= cfgRadiusM;\n const speedOk = speed <= cfgSpeedCap;\n const failsafe = !(geofenceOk && speedOk);\n const safety = {\n geofenceOk,\n speedOk,\n failsafe,\n distanceFromCenterM: Number.isFinite(dist) ? dist : null,\n speedCapMps: cfgSpeedCap\n };\n const row = await appendTelemetry(auth.uid, deviceId, {\n lat,\n lon,\n speed,\n battery: payload?.battery,\n mode: payload?.mode || (failsafe ? \"FAILSAFE_HOLD\" : \"ACTIVE\"),\n raw: payload?.raw || {}\n });\n await updateDevice(auth.uid, deviceId, {\n status: failsafe ? \"failsafe\" : \"active\",\n lastTelemetryAt: row.ts,\n safetyState: safety\n });\n if (failsafe) {\n await addTranscript(auth.uid, {\n source: \"robotics-guardrails\",\n eventType: \"failsafe\",\n text: `Device ${deviceId} failsafe triggered geofenceOk=${geofenceOk} speedOk=${speedOk}`,\n roomId: \"studio-main\",\n meta: { deviceId, safety }\n });\n }\n sendJson(res, 200, { ok: true, telemetry: row, safety, command: failsafe ? \"FAILSAFE_HOLD\" : \"CONTINUE\" });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"telemetry_ingest_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listTelemetry(auth.uid, 150);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createAutomation(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"automation-scheduled\",\n text: `Automation scheduled: ${row.title} at ${new Date(row.scheduleAt).toLocaleString()}`,\n roomId: \"studio-main\",\n meta: { automationId: row.id, action: row.action, deviceId: row.deviceId }\n });\n sendJson(res, 200, { ok: true, automation: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"automation_create_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAutomations(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listDevices(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const devs = await listDevices(auth.uid, 500);\n const d = devs.find((x) => x.id === deviceId);\n if (!d) return sendJson(res, 404, { ok: false, error: \"device_not_found\" });\n const action = String(payload?.action || \"\");\n const caps = Array.isArray(d.capabilityProfile) ? d.capabilityProfile : [];\n if (action && caps.length && !caps.includes(action) && !caps.includes(\"*\")) {\n return sendJson(res, 400, { ok: false, error: \"unsupported_action_for_device\", capabilityProfile: caps });\n }\n const cmd = await enqueueCommand(auth.uid, {\n deviceId,\n action: action || \"NOOP\",\n params: payload?.params || {},\n adapter: payload?.adapter || d.adapter || \"websocket\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-command-queue\",\n eventType: \"command-queued\",\n text: `${cmd.deviceId} -> ${cmd.action}`,\n roomId: \"studio-main\",\n meta: { commandId: cmd.id, adapter: cmd.adapter }\n });\n sendJson(res, 200, { ok: true, command: cmd });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listCommands(auth.uid, 200);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/robotics/commands/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updateCommandStatus(auth.uid, id, payload?.status || \"sent\");\n sendJson(res, 200, { ok: true, command: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/rhizoh/bridge\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const result = await runRhizohBrainV2({\n message: String(payload?.message || \"robotics bridge command\"),\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n worldState: payload?.worldState || {},\n deviceState: payload?.deviceState || {},\n intent: \"robotics-mechanics\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-bridge\",\n eventType: \"robotics-command\",\n text: `Bridge -> ${String(result.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { directive: result.directive, agentId: payload?.agentId || \"\" }\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"robotics_bridge_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/planner/graph\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const graph = buildTaskActionGraph(String(payload?.task || \"\"), payload?.context || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"planner-graph\",\n text: `Planner graph built: ${graph.taskTitle} nodes=${graph.nodes.length}`,\n roomId: \"studio-main\",\n meta: { taskTitle: graph.taskTitle, nodeCount: graph.nodes.length }\n });\n sendJson(res, 200, { ok: true, graph });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"planner_graph_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerSocialChannel(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, channel: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_channel_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listSocialChannels(auth.uid, 100);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/broadcast\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const text = String(payload?.text || \"\").slice(0, 1200);\n const channels = await listSocialChannels(auth.uid, 100);\n const enabled = channels.filter((c) => c.status === \"enabled\");\n const deliveries = [];\n for (const c of enabled) {\n const out = await deliverSocialWithRetry(c, text);\n deliveries.push({ id: c.id, platform: c.platform, ...out });\n }\n const okCount = deliveries.filter((d) => d.ok).length;\n await addTranscript(auth.uid, {\n source: \"social-layer\",\n eventType: \"social-broadcast\",\n text: `Broadcast delivered ${okCount}/${enabled.length} channel(s): ${text.slice(0, 240)}`,\n roomId: \"studio-main\",\n meta: { deliveries }\n });\n sendJson(res, 200, { ok: true, sentChannels: okCount, totalChannels: enabled.length, deliveries });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_broadcast_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/ethics/programs\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, ...buildEthicsPrograms() });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands/dispatch\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const all = await listCommands(auth.uid, 200);\n const queued = all.filter((c) => String(c.status || \"queued\") === \"queued\").slice(0, 50);\n const devices = await listDevices(auth.uid, 500);\n const byId = new Map(devices.map((d) => [d.id, d]));\n const results = [];\n for (const cmd of queued) {\n const dev = byId.get(cmd.deviceId);\n if (!dev) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: \"device_not_found\" });\n continue;\n }\n try {\n const dispatched = await dispatchCommandToAdapter(dev, cmd);\n await updateCommandStatus(auth.uid, cmd.id, \"sent\");\n results.push({ id: cmd.id, ok: true, adapter: dispatched.adapter, via: dispatched.via });\n } catch (error) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: String(error?.message || \"dispatch_failed\") });\n }\n }\n await addTranscript(auth.uid, {\n source: \"robotics-command-dispatch\",\n eventType: \"command-dispatch-batch\",\n text: `Dispatch batch size=${results.length} ok=${results.filter((x) => x.ok).length}`,\n roomId: \"studio-main\",\n meta: { results: results.slice(0, 30) }\n });\n sendJson(res, 200, { ok: true, processed: results.length, results });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"dispatch_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/pdf/jobs/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n const job = await getPdfJob(auth.uid, id);\n if (!job) return sendJson(res, 404, { ok: false, error: \"pdf_job_not_found\" });\n sendJson(res, 200, { ok: true, job });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const row = await appendMemory({\n scope,\n id,\n text: payload?.text,\n tags: payload?.tags || [],\n importance: payload?.importance,\n kind: payload?.kind || \"episodic\",\n meta: { uid: auth.uid, ...(payload?.meta || {}) }\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.externalTruth) {\n try {\n if (process.env.RHIZOH_EXTERNAL_TRUTH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_truth:${ip}`, RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n sendJson(res, 200, buildRhizohExternalGroundTruthPayload(req));\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.productOutcome) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const rawBody = await readHttpBodyText(req, 64 * 1024);\n const body = rawBody.trim() ? safeJsonParse(rawBody) : null;\n if (!body || typeof body !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"invalid_json_payload\" });\n return;\n }\n const sigHdr = String(req.headers?.[\"x-rhizoh-outcome-signature\"] || \"\").trim();\n const hmacV = verifyRhizohOutcomeHmac(rawBody, sigHdr);\n if (!hmacV.ok) {\n sendJson(res, 401, { ok: false, error: hmacV.error || \"outcome_hmac_invalid\" });\n return;\n }\n const srcTokHdr = String(req.headers?.[\"x-rhizoh-outcome-source-token\"] || \"\").trim();\n const srcV = verifyRhizohOutcomeSourceToken(body.source, srcTokHdr);\n if (!srcV.ok) {\n sendJson(res, 401, { ok: false, error: srcV.error || \"outcome_source_token_invalid\" });\n return;\n }\n const subjectKey = String(body.subjectKey || \"\").trim().slice(0, 128);\n if (\n subjectKey &&\n !checkHttpRateLimit(\n `rhizoh_product_outcome_subj:${subjectKey}`,\n RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN,\n 60_000\n )\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_subject\" });\n return;\n }\n const result = ingestRhizohProductOutcomeHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n schemaVersion: \"1.1.0\",\n aggregate: result.aggregate,\n integrity: result.integrity\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"outcome_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.productOutcomeAggregate) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome_agg:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const u = new URL(String(req.url || \"/\"), \"http://localhost\");\n const cohortId = u.searchParams.get(\"cohortId\") || \"\";\n const decisionFingerprint = u.searchParams.get(\"decisionFingerprint\") || \"\";\n const limit = u.searchParams.get(\"limit\") || \"50\";\n const rows = listRhizohProductOutcomeAggregates({\n cohortId: cohortId || undefined,\n decisionFingerprint: decisionFingerprint || undefined,\n limit: Number(limit) || 50\n });\n sendJson(res, 200, { ok: true, schemaVersion: \"1.1.0\", rows });\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.externalLossBatch) {\n try {\n if (process.env.RHIZOH_EXTERNAL_LOSS_BATCH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_loss_batch:${ip}`, RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const result = ingestRhizohExternalLossBatchHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, { ok: true, accepted: result.accepted ?? 0 });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"batch_failed\") });\n }\n return;\n }\n\n function moderationAdminOk(req) {\n const expected = String(process.env.CASTLE_MODERATION_ADMIN_KEY || \"\").trim();\n if (!expected) return false;\n const got = String(req.headers[\"x-castle-moderation-key\"] || \"\").trim();\n return got.length > 0 && got === expected;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.opsAbuseReport) {\n try {\n const body = await readHttpJson(req, 32 * 1024);\n const auth = await resolveHttpUser(req);\n const entry = submitAbuseReportV0({\n reporterUid: auth.ok ? auth.uid : null,\n targetUid: body?.targetUid ?? null,\n category: body?.category,\n detail: body?.detail,\n traceId: body?.traceId ?? null\n });\n sendJson(res, 202, { ok: true, reportId: entry.id, status: entry.status });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsModerationQueue) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n queue: listModerationQueueV0(100),\n softBlocked: listSoftBlockedUsersV0()\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-block\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const out = softBlockUserV0(body?.uid, body?.reason);\n sendJson(res, out.ok ? 200 : 400, out);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-unblock\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n sendJson(res, 200, removeSoftBlockV0(body?.uid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsAgentSnapshots) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n const limit = Number(new URL(req.url || \"\", \"http://local\").searchParams.get(\"limit\") || 50);\n sendJson(res, 200, { ok: true, snapshots: listRecentAgentSnapshotsV0(limit) });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.academicObservatoryExport) {\n const auth = await resolveHttpUser(req);\n const out = handleAcademicObservatoryExportGetV0(req, auth);\n sendJson(res, out.status, out.body);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsHardeningStatus) {\n const auth = await resolveHttpUser(req);\n const principal = auth.ok ? `uid:${auth.uid}` : null;\n const gclSnap = await getGlobalCostLedgerSnapshotV0(principal);\n const economicStrategy = evaluateEconomicStrategyL3V0(gclSnap);\n const hardeningTenantId = resolveHardeningTenantIdV0(auth, req);\n const unifiedState = await buildUnifiedStateNarrativeV0({\n tenantId: hardeningTenantId ?? undefined,\n platformScope: !hardeningTenantId,\n principal\n });\n sendJson(res, 200, {\n ok: true,\n schema: \"rhizoh.ops.hardening_status.v0\",\n containment: readAgentContainmentConfigV0(),\n cost: readCostContainmentConfigV0(),\n gcl: {\n backend: await getGclBackendStatusV0(),\n snapshot: gclSnap\n },\n economicStrategyL3: economicStrategy,\n phasedRollout: await getPhasedRolloutStatsV0(),\n tier: readPhasedRolloutTierV0(),\n principalCost: auth.ok ? await getCostContainmentStatsV0(principal) : null,\n stressTaxonomy: verifyStressTaxonomyCoverageV0(),\n stressConflictResolution: verifyStressConflictResolutionV0(),\n resolutionStabilityEnvelope: verifyResolutionStabilityEnvelopeV0(),\n unifiedState: {\n tenantScope: {\n tenantId: unifiedState.tenantScope?.tenantId,\n scope: unifiedState.tenantScope?.scope,\n noGlobalDerivedState: unifiedState.tenantScope?.noGlobalDerivedState\n },\n narrativeFingerprint: unifiedState.narrativeFingerprint,\n screenshotScopeWatermark: unifiedState.screenshotScopeWatermark,\n interpretationSafetyContract: unifiedState.interpretationSafetyContract,\n interpretationUxContract: {\n schema: unifiedState.interpretationUxContract?.schema,\n layerOrder: unifiedState.interpretationUxContract?.layerOrder,\n narrativeNeverVisuallyDominant: unifiedState.interpretationUxContract?.narrativeNeverVisuallyDominant\n },\n governance: {\n narrativeIsNotDecisionLayer: unifiedState.governance.narrativeIsNotDecisionLayer,\n displayOrder: unifiedState.governance.displayOrder,\n trustPosture: unifiedState.governance.trustPosture,\n interpretationUxContract: unifiedState.governance.interpretationUxContract?.schema\n },\n humanOps: {\n decisionTier: unifiedState.humanOps?.humanDecisionScaling?.tier,\n decisionOwner: unifiedState.humanOps?.humanDecisionScaling?.decisionOwner,\n opsRouteId: unifiedState.humanOps?.humanDecisionOpsRunbook?.routing?.routeId,\n slaAckMinutes: unifiedState.humanOps?.humanDecisionOpsRunbook?.sla?.ackMinutes,\n uiDisclaimerRequired: unifiedState.humanOps?.narrativeUiSafety?.displayRules?.requireDisclaimerBanner,\n misreadHighResidual: unifiedState.humanOps?.misreadSimulation?.highResidualCount,\n propagationHighResidual: unifiedState.humanOps?.socialPropagationSimulation?.highResidualCount,\n worstWatermarkSurvivability:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.worstWatermarkSurvivability,\n dominantDistortionSource:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.dominantDistortionSource\n },\n culturalRisk: {\n perceptualRiskClass: unifiedState.culturalRisk?.perceptualRiskClass,\n trustFork: unifiedState.culturalRisk?.trustDynamics?.fork,\n trustDecayScore: unifiedState.culturalRisk?.trustDynamics?.scores?.trustDecay,\n mythologyScore: unifiedState.culturalRisk?.trustDynamics?.scores?.mythology,\n confidenceDetachmentExposure:\n unifiedState.culturalRisk?.trustDynamics?.axes?.confidenceDetachmentExposure\n },\n appliedSystemsLayer: {\n schema: unifiedState.appliedSystemsLayer?.schema,\n researchOutputType: unifiedState.appliedSystemsLayer?.researchGovernance?.outputType,\n roboticsBlockActuation: unifiedState.appliedSystemsLayer?.roboticsGrounding?.blockActuation,\n spiralRumorCount:\n unifiedState.appliedSystemsLayer?.spiralMMOGameKernel?.gameLayer?.rumors?.length\n },\n actionSemanticGovernance: {\n schema: unifiedState.actionSemanticGovernance?.schema,\n highRiskActionCount: unifiedState.actionSemanticGovernance?.highRiskActionCount,\n loopClosed: unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.loopClosed,\n authorityCollapseMitigated:\n unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.risks?.authorityCollapse\n ?.mitigated\n },\n actionContextResolution: {\n schema: unifiedState.actionContextResolution?.schema,\n contextFingerprint: unifiedState.actionContextResolution?.contextFingerprint?.fingerprintHash,\n domainCollision: unifiedState.actionContextResolution?.domainCollision?.detected,\n deployAgentSpiralBlocked:\n unifiedState.actionContextResolution?.actionOverloadingRisk?.deployAgentExample\n ?.spiral_mmo === \"blocked\",\n roboticsActuationEligible:\n unifiedState.actionContextResolution?.roboticsActuationCorrectness?.actuationEligible\n },\n epistemicCoherence: {\n schema: unifiedState.epistemicCoherence?.schema,\n verdict: unifiedState.epistemicCoherence?.systemCoherence?.verdict,\n coherenceScore: unifiedState.epistemicCoherence?.systemCoherence?.score,\n contradictionCount: unifiedState.epistemicCoherence?.crossLayerContradictions?.count,\n operatorDecision: unifiedState.epistemicCoherence?.uxCompression?.operatorDecision,\n headlineTr: unifiedState.epistemicCoherence?.uxCompression?.headline?.tr,\n falseSafetyWarning: Boolean(\n unifiedState.epistemicCoherence?.uxCompression?.falseSafetyWarning\n )\n },\n coherenceAuthorityBoundary: {\n schema: unifiedState.coherenceAuthorityBoundary?.schema,\n bindingValid: unifiedState.coherenceAuthorityBoundary?.eclAuthorityBinding?.valid,\n canExecute: unifiedState.coherenceAuthorityBoundary?.contract?.can_execute,\n mandatoryBannerTr:\n unifiedState.coherenceAuthorityBoundary?.disclaimers?.mandatoryBanner?.tr\n },\n decisionLatencyGovernance: {\n schema: unifiedState.decisionLatencyGovernance?.schema,\n latencyTier: unifiedState.decisionLatencyGovernance?.latencyTier,\n inflationScore: unifiedState.decisionLatencyGovernance?.latencyInflation?.score,\n fastPathEligible: unifiedState.decisionLatencyGovernance?.fastPath?.eligible,\n primaryActionId: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.primaryActionId,\n ackSlaMinutes: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.ackSlaMinutes,\n certaintyCap: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.certaintyCap,\n uncertaintyItems:\n unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.uncertaintyEnvelope?.itemCount\n },\n decisionPacketUncertaintyBoundary: {\n schema: unifiedState.decisionPacketUncertaintyBoundary?.schema,\n invariantValid: unifiedState.decisionPacketUncertaintyBoundary?.invariantCheck?.valid,\n centralInvariant: unifiedState.decisionPacketUncertaintyBoundary?.centralInvariant\n },\n epistemicDecisionPacing: {\n schema: unifiedState.epistemicDecisionPacing?.schema,\n queueSaturationTier:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation?.tier,\n operatorProcessingLatency:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation\n ?.operatorProcessingLatency,\n postureWindowCount:\n unifiedState.epistemicDecisionPacing?.temporaryStaticPostureWindows\n ?.temporaryStaticPostureWindows?.windowCount,\n executionBoundariesValid:\n unifiedState.epistemicDecisionPacing?.executionBoundaries?.valid,\n grantsExecutionApproval: unifiedState.epistemicDecisionPacing?.grantsExecutionApproval\n },\n epistemicTemporalCoherence: {\n schema: unifiedState.epistemicTemporalCoherence?.schema,\n allInvariantsValid: unifiedState.epistemicTemporalCoherence?.allInvariantsValid,\n temporalAlignmentValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.temporalAlignment?.valid,\n crossWindowGuardValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard?.valid,\n delayedTruthValid: unifiedState.epistemicTemporalCoherence?.invariants?.delayedTruthRule?.valid,\n reconciliationRequired:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard\n ?.reconciliation?.required\n },\n realityDriftObserver: {\n schema: unifiedState.realityDriftObserver?.schema,\n driftActive: unifiedState.realityDriftObserver?.driftObserverSummary?.active,\n misapprehensionShapeCount:\n unifiedState.realityDriftObserver?.misapprehensionShapeCatalog?.shapeCount,\n criticalShapeCount:\n unifiedState.realityDriftObserver?.driftObserverSummary?.criticalShapeCount,\n roboticsMismatchActive:\n unifiedState.realityDriftObserver?.roboticsFeedbackMismatch?.active\n },\n driftCausality: {\n schema: unifiedState.driftCausality?.schema,\n shapesWithCausalExplanation: unifiedState.driftCausality?.rdolLinkage?.shapesWithCausalExplanation,\n activeDomainCount: unifiedState.driftCausality?.causalitySummary?.activeDomainCount,\n propagationCausalityActive: unifiedState.driftCausality?.causalDomains?.propagation?.active\n },\n productReadiness: unifiedState.productReadiness,\n stateLayers: {\n raw: unifiedState.stateLayers.raw,\n derived: {\n systemState: unifiedState.stateLayers.derived.systemState,\n narrative: unifiedState.stateLayers.derived.narrative,\n validation: unifiedState.stateLayers.derived.validation\n },\n policy: {\n binding: unifiedState.stateLayers.policy.binding,\n decisionOwner: unifiedState.stateLayers.policy.decisionOwner,\n prohibitedActions: unifiedState.stateLayers.policy.prohibitedActions\n }\n },\n systemState: unifiedState.systemState,\n interpretation: unifiedState.interpretation,\n validation: {\n trustPosture: unifiedState.validation.trustPosture,\n adjustedConfidence: unifiedState.validation.adjustedConfidence,\n agreement: unifiedState.validation.confidenceDecomposition.agreement,\n divergenceScore: unifiedState.validation.divergence.divergenceScore,\n validated: unifiedState.validation.divergence.validated,\n uncertaintyTags: unifiedState.validation.uncertainty.tags.map((t) => t.id),\n flags: unifiedState.validation.divergence.flags.map((f) => f.id)\n },\n compression: unifiedState.compression\n }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 6 * 1024 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (\n !checkHttpRateLimit(`rhizoh_voice_transcribe:${rlKey}`, RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN, 60_000)\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const voiceEnv = rhizohVoiceTranscribeEnvV3();\n if (!voiceEnv.any) {\n sendJson(res, 503, {\n ok: false,\n error: \"voice_asr_not_configured\",\n env: voiceEnv\n });\n return;\n }\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n const result = await runRhizohVoiceTranscribeV3(\n applyLanguagePropagationToVoiceBodyV1(payload, langProp)\n );\n sendJson(res, result.ok ? 200 : 422, {\n ...result,\n traceId: String(payload?.traceId || \"\"),\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n atMs: Date.now()\n });\n } catch (error) {\n sendJson(res, 500, {\n ok: false,\n error: \"voice_transcribe_failed\",\n detail: String(error?.message || error),\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(\"/rhizoh/llm/task/\")) {\n try {\n const rawTaskId = decodeURIComponent(pathname.slice(\"/rhizoh/llm/task/\".length).split(\"/\")[0] || \"\");\n const taskId = String(rawTaskId || \"\").toLowerCase() === \"last\" ? getLatestLlmWorkerTaskIdV0() : rawTaskId;\n const snap = taskId ? getLlmWorkerTaskSnapshotV0(taskId) : null;\n if (!snap) {\n sendJson(res, 404, {\n ok: false,\n error: \"llm_task_not_found\",\n taskId: rawTaskId,\n status: \"missing\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n if (snap.status === \"processing\") {\n sendJson(res, 202, {\n ok: true,\n taskId: snap.taskId,\n status: \"processing\",\n createdAt: snap.createdAt,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n // Terminal states (completed/failed) always HTTP 200 — poll must not look like server fault (500).\n sendJson(res, 200, {\n ...(snap.body || { ok: false, error: \"llm_task_empty\", taskId: snap.taskId }),\n taskId: snap.taskId,\n status: snap.status === \"completed\" ? \"completed\" : snap.status,\n terminal: true,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n } catch (error) {\n console.error(\"[llm-task-poll] handler failed:\", error);\n sendJson(res, 200, {\n ok: false,\n error: \"llm_task_poll_handler_failed\",\n detail: String(error?.message || error),\n status: \"failed\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === rhizohRuntime.routes.rhizohLlm) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 128 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n logRhizohHealth(\"gateway_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: String(payload?.traceId || \"\"),\n auth: auth.ok ? \"ok\" : \"anon\",\n ip\n });\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (!checkHttpRateLimit(`rhizoh_llm:${rlKey}`, RL_RHIZOH_LLM_PER_MIN, 60_000)) {\n const rlTaxonomy = classifyStressResponseV0({ code: \"rate_limit_exceeded\" });\n return sendJson(res, 429, {\n ok: false,\n error: \"rate_limit_exceeded\",\n reply: rlTaxonomy.userMessageTr || \"İstek sınırı aşıldı. Kısa süre sonra tekrar deneyin.\",\n stressClass: rlTaxonomy.stressClass,\n responseAction: rlTaxonomy.responseAction,\n stressMatrix: rlTaxonomy.matrix,\n stressInterpretable: rlTaxonomy.interpretable,\n stressConfidence: rlTaxonomy.stressConfidence,\n actionConfidence: rlTaxonomy.actionConfidence,\n actionSoftened: rlTaxonomy.actionSoftened,\n responseActionStrict: rlTaxonomy.responseActionStrict,\n actionInterpretable: rlTaxonomy.actionInterpretable,\n conflictResolution: rlTaxonomy.conflictResolution,\n directive: \"NONE\"\n });\n }\n\n const requireExplicit = process.env.CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE === \"1\";\n const keyMode = normalizeClientLlmKeySource(payload?.llmKeySource ?? payload?.keySource);\n if (keyMode === null) {\n return sendJson(res, 400, {\n ok: false,\n error: \"invalid_llm_key_source\",\n reply: \"llmKeySource geçersiz. Kullanın: env | user_connection | auto\",\n directive: \"NONE\"\n });\n }\n if (requireExplicit && keyMode === \"auto\") {\n return sendJson(res, 400, {\n ok: false,\n error: \"llm_key_source_required\",\n reply: \"Sunucu CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE=1: istekte llmKeySource (env veya user_connection) zorunlu.\",\n directive: \"NONE\"\n });\n }\n\n const conn = auth.ok ? await resolveConnection(auth.uid, String(payload?.connectionId || \"\")) : null;\n\n if (keyMode === \"user_connection\") {\n if (!auth.ok) {\n return sendJson(res, 401, {\n ok: false,\n error: \"llm_auth_required_for_user_connection\",\n reply: \"Kayıtlı LLM bağlantısı için giriş gerekli.\",\n directive: \"NONE\"\n });\n }\n if (!conn?.apiKey) {\n return sendJson(res, 400, {\n ok: false,\n error: \"user_llm_connection_required\",\n reply: \"Geçerli bir LLM bağlantısı yok; Studio’da bağlantı ekleyin veya llmKeySource=env kullanın.\",\n directive: \"NONE\"\n });\n }\n }\n\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n let safePayload = applyLanguagePropagationToLlmPayloadV1({ ...(payload || {}) }, langProp);\n const trimOut = trimRhizohLlmGatewayPayloadV1(safePayload);\n if (trimOut.meta.trimmed) {\n logRhizohHealth(\"llm_payload_trimmed\", {\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n bytesBefore: trimOut.meta.bytesBefore,\n bytesAfter: trimOut.meta.bytesAfter\n });\n }\n safePayload = trimOut.payload;\n delete safePayload.apiKey;\n delete safePayload.llmKeySource;\n delete safePayload.keySource;\n\n const llmMessage = String(\n safePayload?.message || safePayload?.text || payload?.message || payload?.text || \"\"\n ).trim();\n if (!llmMessage) {\n return sendJson(res, 400, {\n ok: false,\n error: \"message_required\",\n reply: \"Mesaj metni gerekli (message alanı boş).\",\n directive: \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n if (!safePayload.message) {\n safePayload = { ...safePayload, message: llmMessage };\n }\n\n let resolvedProvider = payload?.provider;\n let resolvedModel = payload?.model;\n let connApiKey = \"\";\n\n if (keyMode === \"user_connection\") {\n resolvedProvider = conn.provider;\n resolvedModel = conn.model;\n connApiKey = conn.apiKey || \"\";\n } else if (keyMode === \"env\") {\n connApiKey = \"\";\n } else {\n resolvedProvider = conn?.provider || payload?.provider;\n resolvedModel = conn?.model || payload?.model;\n connApiKey = conn?.apiKey || \"\";\n }\n\n const turnInput = {\n safePayload,\n auth,\n keyMode,\n conn,\n resolvedProvider,\n resolvedModel,\n connApiKey\n };\n const turnMeta = {\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n langEcho: buildRhizohLanguagePropagationEchoV1(langProp)\n };\n\n if (\n isLlmWorkerEnabledV0() &&\n isLlmWorkerAsyncEnabledV0() &&\n String(req.headers[\"x-rhizoh-llm-sync\"] || \"\").trim() !== \"1\"\n ) {\n const taskId = enqueueLlmWorkerTaskAsyncV0(turnInput, turnMeta);\n logRhizohHealth(\"llm_worker_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n taskId,\n traceId: String(payload?.traceId || taskId),\n keyMode,\n async: true\n });\n sendJson(res, 202, {\n ok: true,\n taskId,\n status: \"processing\",\n traceId: String(payload?.traceId || taskId),\n pollPath: `/rhizoh/llm/task/${taskId}`,\n gatewayPid: process.pid,\n ...turnMeta.langEcho\n });\n return;\n }\n\n const { result, traceId, turnLatencyMs, spinePhases, sampledTrace } = isLlmWorkerEnabledV0()\n ? await runLlmWorkerTaskV0(turnInput, turnMeta)\n : await rhizohGatewayTurn(turnInput);\n logRhizohHealth(\"llm_response\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId,\n turnLatencyMs: Number(turnLatencyMs || 0),\n keyMode\n });\n sendJson(res, 200, {\n ...result,\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n traceId,\n turnLatencyMs,\n ...(spinePhases ? { spinePhases } : {}),\n ...(sampledTrace !== undefined ? { sampledTrace } : {})\n });\n } catch (error) {\n const msg = String(error?.message || \"\");\n const code = error?.code || \"\";\n let status = 500;\n if (msg === \"payload_too_large\") status = 413;\n else if (msg === \"rate_limit_exceeded\") status = 429;\n else if (\n code === \"cost_hard_limit\" ||\n code === \"phased_rollout_capacity\" ||\n error?.containment === true ||\n String(code).startsWith(\"agent_\")\n ) {\n status = 429;\n } else if (code === \"user_soft_blocked\" || code === \"prompt_abuse_detected\") status = 403;\n else if (\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(code) ||\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(msg) ||\n msg.includes(\"missing_api_key_for_\")\n )\n status = 400;\n let rhizohFailureKind = \"provider_error\";\n if (msg === \"rate_limit_exceeded\" || status === 429) rhizohFailureKind = \"rate_limit\";\n else if (status === 403) rhizohFailureKind = \"policy_block\";\n else if (status === 400 || status === 413) rhizohFailureKind = \"client_config\";\n else if (msg.startsWith(\"provider_http_\")) {\n const st = Number(msg.replace(\"provider_http_\", \"\"));\n if (st === 408 || st === 504) rhizohFailureKind = \"timeout\";\n else rhizohFailureKind = \"provider_error\";\n } else if (/timeout|timed out|ETIMEDOUT/i.test(msg)) rhizohFailureKind = \"timeout\";\n\n const providerHttpMatch = msg.match(/^provider_http_(\\d{3})$/);\n const stressTaxonomy =\n error?.stressClass != null\n ? {\n stressClass: error.stressClass,\n responseAction: error.responseAction,\n stressMatrix: error.stressMatrix,\n stressInterpretable: error.stressInterpretable !== false,\n stressConfidence: error.stressConfidence,\n actionConfidence: error.actionConfidence,\n actionSoftened: error.actionSoftened,\n responseActionStrict: error.responseActionStrict,\n actionInterpretable: error.actionInterpretable,\n conflictResolution: error.conflictResolution,\n stressSecondary: error.stressSecondary,\n responseActions: error.responseActions\n }\n : (() => {\n const t = classifyStressResponseV0({\n code: code || msg,\n rhizohFailureKind,\n providerHttpStatus: providerHttpMatch\n ? Number(providerHttpMatch[1])\n : null\n });\n return {\n stressClass: t.stressClass,\n responseAction: t.responseAction,\n stressMatrix: t.matrix,\n stressInterpretable: t.interpretable,\n stressConfidence: t.stressConfidence,\n actionConfidence: t.actionConfidence,\n actionSoftened: t.actionSoftened,\n responseActionStrict: t.responseActionStrict,\n actionInterpretable: t.actionInterpretable,\n conflictResolution: t.conflictResolution,\n stressSecondary: t.stressSecondary,\n responseActions: t.responseActions\n };\n })();\n sendJson(res, status, {\n ok: false,\n rhizohFailureKind,\n ...(providerHttpMatch ? { providerHttpStatus: Number(providerHttpMatch[1]) } : {}),\n error: code || msg || \"rhizoh_llm_failed\",\n detail: msg || code || undefined,\n reply:\n error?.reply ||\n (status === 500 ? \"Rhizoh bağlantısı geçici olarak kesildi.\" : msg),\n directive: error?.directive || \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n ...stressTaxonomy\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicSeal) {\n try {\n if (!EPISTEMIC_SEAL_SECRET) {\n sendJson(res, 503, { ok: false, error: \"epistemic_seal_disabled\" });\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`epistemic_seal:${ip}`, RL_EPISTEMIC_SEAL_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const truth = body.truth_contract;\n if (!truth || typeof truth !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"truth_contract_required\" });\n return;\n }\n const sealBody = {\n truth_contract: truth,\n runtime_hash: String(body.runtime_hash || \"\"),\n model_route: {\n provider: body.model_route?.provider ?? null,\n model: body.model_route?.model ?? null\n },\n memory_digest: String(body.memory_digest || \"\"),\n world_snapshot_hash: String(body.world_snapshot_hash || \"\"),\n timestamp: Number(body.timestamp) || Date.now()\n };\n const canonical = canonicalEpistemicSealString(sealBody);\n const { hash, signature } = hashAndSignEpistemicSeal(canonical, EPISTEMIC_SEAL_SECRET);\n recordGenesisEpistemicSealIssued(hash);\n const attestation = {\n schema: EPISTEMIC_SEAL_SCHEMA,\n algorithm: \"SHA-256+HMAC-SHA256\",\n keyHint: process.env.CASTLE_EPISTEMIC_SEAL_SECRET ? \"CASTLE_EPISTEMIC_SEAL_SECRET\" : \"CASTLE_GATEWAY_TOKEN\",\n issuedAt: Date.now(),\n gateway: { service: \"castle-gateway\", listenPort: PORT }\n };\n sendJson(res, 200, {\n ok: true,\n hash,\n signature,\n attestation,\n canonicalBytes: Buffer.byteLength(canonical, \"utf8\")\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"seal_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicLogsBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`epistemic_logs:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 120) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const persisted = await persistEpistemicLedgerBatch(auth.uid, entries);\n recordGenesisEpistemicLedgerPersisted(persisted.persisted);\n // Internal cognition layer: forecast stays gateway/ledger-only (no client exposure).\n try {\n await persistEpistemicForecastBatch(auth.uid, persisted.normalized || []);\n } catch {\n /* forecast failures must not break primary ledger ingest */\n }\n sendJson(res, 200, {\n ok: true,\n persisted: persisted.persisted,\n mode: persisted.mode,\n latest: persisted.normalized?.[persisted.normalized.length - 1] || null\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"epistemic_log_ingest_failed\") });\n }\n return;\n }\n\n sendJson(res, 404, { ok: false, error: \"not_found\" });\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\nspiralWssForGenesis = wss;\nconst orchestrator = createOrchestrator();\nconst clientStats = new Map();\nlet broadcasterClientId = null;\nconst spiralState = {\n rooms: [{ id: \"room-default\", name: \"Spiral Default Arena\" }],\n activeByClient: new Map(),\n characters: new Map() // id -> {id,name,role,roomId,source}\n};\n\n/** Castle social rooms — keyed by `castleRoomKey` from {@link WS_MESSAGE.CASTLE_SOCIAL_PULSE}. */\nconst castleSocialRoomState = new Map();\nconst CASTLE_SOCIAL_MEMBER_TTL_MS = 90_000;\nconst CASTLE_SOCIAL_MAX_PULSE_BYTES = 8192;\nconst RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES = 5 * 1024 * 1024;\nconst RHIZOH_VOICE_LIVE_MAX_CHUNKS = 96;\n\n/**\n * Voice Engine v3 live media lane.\n * Client streams MediaRecorder chunks over the existing gateway WS; STOP reuses the\n * gateway ASR runner so orchestration remains the single authority downstream.\n */\nconst rhizohVoiceLiveSessions = new WeakMap();\n\nfunction appendRhizohVoiceLiveEventV0(session, type, payloadRefSeed = \"\") {\n if (!session) return null;\n session.eventSeq = (Number(session.eventSeq) || 0) + 1;\n const event = createRhizohImmutableEventV0({\n type,\n sessionId: session.sessionId,\n traceId: session.traceId,\n actorSeed: session.sessionId || session.traceId,\n eventSeq: session.eventSeq,\n payloadRef: createRhizohPayloadRefV0(payloadRefSeed || `${session.sessionId}:${type}:${session.eventSeq}`)\n });\n session.events = [...(session.events || []), event].slice(-64);\n return event;\n}\n\nfunction sendRhizohVoiceLiveEnvelopeV0(socket, type, payload) {\n try {\n if (socket?.readyState === 1) socket.send(JSON.stringify(createEnvelope(type, payload)));\n } catch {\n /* noop */\n }\n}\n\nfunction decodeRhizohVoiceLiveChunkV0(payload = {}) {\n const b64 = String(payload.audioBase64 || \"\").trim();\n if (!b64) return { ok: false, error: \"audio_base64_required\" };\n try {\n const buf = Buffer.from(b64, \"base64\");\n if (!buf.length) return { ok: false, error: \"audio_decode_empty\" };\n return { ok: true, buffer: buf };\n } catch {\n return { ok: false, error: \"audio_base64_invalid\" };\n }\n}\n\nfunction handleRhizohVoiceLiveStartV0(socket, payload = {}) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const sessionId = String(p.sessionId || `live_${Date.now().toString(36)}`).slice(0, 96);\n rhizohVoiceLiveSessions.set(socket, {\n sessionId,\n traceId: String(p.traceId || \"\").slice(0, 128),\n mimeType: String(p.mimeType || \"audio/webm\").slice(0, 80),\n languageCode: String(p.languageCode || \"tr-TR\").slice(0, 32),\n path: String(p.path || \"both\").slice(0, 24),\n chunks: [],\n chunkIndexes: [],\n totalBytes: 0,\n nextChunkIndex: 1,\n eventSeq: 0,\n events: [],\n startedAtMs: Date.now()\n });\n appendRhizohVoiceLiveEventV0(rhizohVoiceLiveSessions.get(socket), \"VOICE_LIVE_SESSION_STARTED\", `${sessionId}:live_session`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_START, {\n ok: true,\n sessionId,\n schema: \"castle.rhizoh.voice_live.gateway_lane.v0\"\n });\n}\n\nfunction handleRhizohVoiceLiveChunkV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const sequence = validateRhizohVoiceLiveChunkPayloadV0(payload, {\n expectedChunkIndex: session.nextChunkIndex\n });\n if (!sequence.ok) {\n rhizohVoiceLiveSessions.delete(socket);\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:chunk_reject`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: sequence.error,\n expectedChunkIndex: sequence.expectedChunkIndex,\n chunkIndex: sequence.chunkIndex\n });\n return;\n }\n const decoded = decodeRhizohVoiceLiveChunkV0(payload);\n if (!decoded.ok) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: decoded.error\n });\n return;\n }\n if (\n session.chunks.length >= RHIZOH_VOICE_LIVE_MAX_CHUNKS ||\n session.totalBytes + decoded.buffer.length > RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES\n ) {\n rhizohVoiceLiveSessions.delete(socket);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_audio_too_large\"\n });\n return;\n }\n session.chunks.push(decoded.buffer);\n session.chunkIndexes.push(sequence.chunkIndex);\n session.totalBytes += decoded.buffer.length;\n session.nextChunkIndex += 1;\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_CHUNK_ACCEPTED\", `${session.sessionId}:chunk:${sequence.chunkIndex}`);\n}\n\nfunction handleRhizohVoiceLiveStopV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n rhizohVoiceLiveSessions.delete(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const lastChunkIndex = Number(payload?.lastChunkIndex);\n if (Number.isInteger(lastChunkIndex) && lastChunkIndex !== session.chunks.length) {\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:stop_mismatch`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_chunk_count_mismatch\",\n expectedChunkCount: lastChunkIndex,\n receivedChunkCount: session.chunks.length\n });\n return;\n }\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_STOP_RECEIVED\", `${session.sessionId}:stop:${session.chunks.length}`);\n const audio = Buffer.concat(session.chunks);\n if (!audio.length) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"audio_empty\"\n });\n return;\n }\n runRhizohVoiceTranscribeV3({\n path: String(payload?.path || session.path || \"both\"),\n audioBase64: audio.toString(\"base64\"),\n mimeType: String(payload?.mimeType || session.mimeType || \"audio/webm\"),\n languageCode: String(payload?.languageCode || session.languageCode || \"tr-TR\"),\n traceId: String(payload?.traceId || session.traceId || \"\"),\n sessionId: session.sessionId,\n transport: \"gateway_ws_live\"\n })\n .then((result) => {\n appendRhizohVoiceLiveEventV0(session, result?.ok ? \"VOICE_TRANSCRIBE_FINAL\" : \"VOICE_TRANSCRIBE_FAILED\", `${session.sessionId}:final`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_FINAL, {\n ...result,\n sessionId: session.sessionId,\n transportPath: \"gateway_ws_live\",\n bytes: audio.length,\n chunkCount: session.chunks.length,\n chunkSequence: Object.freeze({\n ok: true,\n firstChunkIndex: session.chunkIndexes[0] || null,\n lastChunkIndex: session.chunkIndexes[session.chunkIndexes.length - 1] || null,\n receivedChunkCount: session.chunks.length\n }),\n eventTrace: Object.freeze((session.events || []).slice(-16)),\n recordedMs: Math.max(0, Date.now() - session.startedAtMs)\n });\n })\n .catch((e) => {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_transcribe_failed\",\n detail: String(e?.message || e)\n });\n });\n}\n\nfunction removeCastleSocialClientFromAllRooms(clientId) {\n const cid = String(clientId || \"\");\n if (!cid) return;\n for (const room of castleSocialRoomState.values()) {\n room.members.delete(cid);\n }\n}\n\nfunction buildCastleSocialRoomPayload(roomKey) {\n const room = castleSocialRoomState.get(roomKey);\n if (!room) return null;\n const now = Date.now();\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const roster = [...room.members.values()].map((m) => ({\n userId: m.userId,\n lastMs: m.lastMs,\n ...m.sp\n }));\n const peerFeeds =\n room.walPeers && room.walPeers.size\n ? [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }))\n : [];\n return { castleRoomKey: roomKey, seq: room.seq, roster, peerFeeds, ts: now };\n}\n\nconst CASTLE_WAL_PEER_MAX_FEED_BYTES = 48 * 1024;\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {{ ok: boolean, code?: string, reason?: string }} gate\n */\nfunction rejectWalPeerIngressV0(socket, gate) {\n const code = gate.code || \"wal_peer_rejected\";\n recordWalPeerRejectV0(code);\n recordSubstrateOtelEventV0(\"wal_peer_reject\", { code, reason: String(gate.reason || \"\").slice(0, 120) });\n socket.send(\n JSON.stringify(\n createEnvelope(WS_MESSAGE.ERROR, {\n error: gate.reason || \"WAL peer ingress rejected.\",\n code\n })\n )\n );\n}\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {Record<string, unknown> | null | undefined} walPeerFeed\n * @returns {boolean}\n */\nfunction gateWalPeerIngressV0(socket, walPeerFeed) {\n const authGate = assertWalPeerSocketAuthorityV0(socket);\n if (!authGate.ok) {\n rejectWalPeerIngressV0(socket, authGate);\n return false;\n }\n const sigGate = validateWalPeerFeedSignatureV0(\n walPeerFeed && typeof walPeerFeed === \"object\" ? walPeerFeed : {}\n );\n if (!sigGate.ok) {\n rejectWalPeerIngressV0(socket, sigGate);\n return false;\n }\n return true;\n}\n\nfunction handleCastleWalPeerFeed(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_WAL_PEER_MAX_FEED_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_WAL_PEER_FEED too large.\" })));\n return;\n }\n const walPeerFeed = p.walPeerFeed && typeof p.walPeerFeed === \"object\" ? p.walPeerFeed : {};\n if (!gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const castleRoomKey = String(p.castleRoomKey || \"wal:convergence\").slice(0, 64);\n const castleId = String(p.castleId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId,\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: room.members.get(socket.clientId)?.sp ?? {}\n });\n const peerFeeds = [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }));\n const body = {\n castleRoomKey,\n seq: room.seq,\n peerFeeds,\n ts: now\n };\n const env = createEnvelope(WS_MESSAGE.CASTLE_WAL_PEER_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys(), ...room.walPeers.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction handleCastleSocialPulse(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_SOCIAL_MAX_PULSE_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_SOCIAL_PULSE too large.\" })));\n return;\n }\n const castleRoomKey = String(p.castleRoomKey || \"default\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n const sp = p.socialPulse && typeof p.socialPulse === \"object\" ? p.socialPulse : {};\n const walPeerFeed =\n p.walPeerFeed && typeof p.walPeerFeed === \"object\"\n ? p.walPeerFeed\n : sp.walPeerFeed && typeof sp.walPeerFeed === \"object\"\n ? sp.walPeerFeed\n : null;\n if (walPeerFeed && !gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const spNext = walPeerFeed ? { ...sp, walPeerFeed } : { ...sp };\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: spNext\n });\n if (walPeerFeed) {\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId: String(p.castleId || userId).slice(0, 64),\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n }\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const body = buildCastleSocialRoomPayload(castleRoomKey);\n if (!body) return;\n const env = createEnvelope(WS_MESSAGE.CASTLE_SOCIAL_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcast(msgObj) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcastState() {\n broadcast(\n createEnvelope(WS_MESSAGE.BROADCAST_STATE, {\n broadcasterClientId,\n viewers: [...wss.clients].filter((c) => c.readyState === 1).length\n })\n );\n}\n\nfunction currentSpiralStatePayload() {\n return {\n rooms: spiralState.rooms,\n characters: [...spiralState.characters.values()]\n };\n}\n\nfunction broadcastSpiralState() {\n broadcast(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload()));\n}\n\nfunction ensureRoom(roomId, fallbackName = null) {\n if (!roomId) return spiralState.rooms[0].id;\n const exists = spiralState.rooms.find((r) => r.id === roomId);\n if (exists) return exists.id;\n const created = { id: roomId, name: fallbackName || `Spiral-${roomId.slice(-4)}` };\n spiralState.rooms.push(created);\n return created.id;\n}\n\nfunction syncSpiralCharactersWithWorld(snapshot) {\n const agents = snapshot?.agents || [];\n for (const a of agents) {\n if (!spiralState.characters.has(a.id)) {\n spiralState.characters.set(a.id, {\n id: a.id,\n name: a.id,\n role: \"agent\",\n roomId: spiralState.rooms[0].id,\n source: \"world\"\n });\n }\n }\n}\n\nfunction broadcastExcept(msgObj, exceptSocket) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client !== exceptSocket && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction validateInputFrame(payload, socket) {\n const stats = clientStats.get(socket.clientId) || { lastFrame: 0, windowStart: Date.now(), msgCount: 0 };\n const now = Date.now();\n if (now - stats.windowStart > 5000) {\n stats.windowStart = now;\n stats.msgCount = 0;\n }\n stats.msgCount += 1;\n clientStats.set(socket.clientId, stats);\n if (stats.msgCount > 120) return \"Rate limit exceeded.\";\n\n if (!payload || !Number.isInteger(payload.frame)) return \"frame must be integer.\";\n if (payload.frame <= stats.lastFrame) return \"frame must be monotonic.\";\n const commands = Array.isArray(payload.commands) ? payload.commands : [];\n if (commands.length > 8) return \"Too many commands in one frame.\";\n for (const cmd of commands) {\n if (!cmd || typeof cmd.command !== \"string\") return \"Invalid command format.\";\n if (\n cmd.command !== \"SPAWN_AGENT\" &&\n cmd.command !== \"LIST_CASTLES\" &&\n cmd.command !== \"SPAWN_ENTITY\" &&\n cmd.command !== \"CREATE_CASTLE\"\n ) return \"Unsupported command.\";\n }\n stats.lastFrame = payload.frame;\n clientStats.set(socket.clientId, stats);\n return null;\n}\n\nfunction validateSignalPayload(payload) {\n if (!payload || typeof payload !== \"object\") return \"Invalid signaling payload.\";\n const signalType = payload.signalType;\n if (![\"OFFER\", \"ANSWER\", \"ICE\"].includes(signalType)) return \"Invalid signal type.\";\n if (typeof payload.to !== \"string\" || payload.to.length < 3) return \"Invalid signal destination.\";\n if (signalType !== \"ICE\" && JSON.stringify(payload.sdp || {}).length > 16 * 1024) return \"SDP too large.\";\n if (signalType === \"ICE\" && JSON.stringify(payload.candidate || {}).length > 6 * 1024) return \"ICE too large.\";\n return null;\n}\n\nfunction sanitizeRawMessage(raw) {\n if (!raw) return null;\n const text = raw.toString();\n if (Buffer.byteLength(text, \"utf8\") > MAX_MESSAGE_BYTES) return null;\n return text;\n}\n\nwss.on(\"connection\", async (socket, req) => {\n const origin = req?.headers?.origin || \"\";\n if (!httpCors.isWebSocketOriginAllowed(origin)) {\n socket.close(1008, \"Origin not allowed\");\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const path = req?.url || \"\";\n const query = new URL(`ws://localhost${path}`).searchParams;\n const token = query.get(\"token\");\n if (!token || token !== REQUIRED_GATEWAY_TOKEN) {\n socket.close(1008, \"Invalid gateway token\");\n return;\n }\n }\n if (REQUIRE_AUTH) {\n const result = await verifyClientToken(req);\n if (!result.ok) {\n const isLocal = origin.includes(\"localhost\") || origin.includes(\"127.0.0.1\") || !origin;\n if (!(ALLOW_DEV_ANON && isLocal)) {\n socket.close(1008, result.reason);\n return;\n }\n socket.auth = { ok: true, kind: \"dev-anon\", user: { uid: \"dev-anon\" } };\n } else {\n socket.auth = result;\n }\n }\n\n socket.clientId = `c-${Math.random().toString(36).slice(2, 8)}`;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.HELLO, { message: \"Castle Gateway online.\", clientId: socket.clientId })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.WORLD_SNAPSHOT, orchestrator.worldSnapshot())));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.BROADCAST_STATE, { broadcasterClientId, viewers: [...wss.clients].filter((c) => c.readyState === 1).length })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload())));\n broadcastExcept(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }), socket);\n\n socket.on(\"message\", (raw) => {\n const safeRaw = sanitizeRawMessage(raw);\n if (!safeRaw) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Message too large or invalid.\" })));\n return;\n }\n const parsed = safeJsonParse(safeRaw);\n if (!parsed?.type) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid message.\" })));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND_TEXT) {\n const canonical = parseTextToCommand(parsed.payload?.text);\n if (!canonical) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Unknown command text.\" })));\n return;\n }\n const result = orchestrator.applyCommand(canonical);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND) {\n const result = orchestrator.applyCommand(parsed.payload);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n }\n\n if (parsed.type === WS_MESSAGE.INPUT_FRAME) {\n const err = validateInputFrame(parsed.payload, socket);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const frame = parsed.payload.frame;\n const commands = Array.isArray(parsed.payload?.commands) ? parsed.payload.commands : [];\n orchestrator.queueInputFrame(socket.clientId, frame, commands);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, queued: true, frame })));\n }\n\n if (parsed.type === WS_MESSAGE.SIGNAL) {\n const payload = parsed.payload || {};\n const err = validateSignalPayload(payload);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const signalType = payload.signalType;\n const to = payload.to;\n for (const client of wss.clients) {\n if (client.clientId === to && client.readyState === 1) {\n client.send(JSON.stringify(createEnvelope(WS_MESSAGE.SIGNAL, { ...payload, from: socket.clientId })));\n break;\n }\n }\n }\n\n if (parsed.type === WS_MESSAGE.DRONE_TELEMETRY) {\n const payload = parsed.payload || {};\n const json = JSON.stringify(payload);\n if (Buffer.byteLength(json, \"utf8\") > 48 * 1024) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"DRONE_TELEMETRY payload too large.\" })));\n return;\n }\n broadcastExcept(createEnvelope(WS_MESSAGE.DRONE_TELEMETRY, { ...payload, relayFrom: socket.clientId }), socket);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_START) {\n handleRhizohVoiceLiveStartV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_CHUNK) {\n handleRhizohVoiceLiveChunkV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_STOP) {\n handleRhizohVoiceLiveStopV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_SOCIAL_PULSE) {\n handleCastleSocialPulse(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_WAL_PEER_FEED) {\n handleCastleWalPeerFeed(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.OPEN_DATA_QUERY) {\n queryOpenData(parsed.payload)\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: true, result })));\n })\n .catch((e) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: false, error: e.message })));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_QUERY) {\n runRhizohBrainV2({\n ...(parsed.payload || {}),\n uid: socket?.auth?.user?.uid || \"unknown\"\n })\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, result)));\n })\n .catch(async () => {\n const fallback = await runRhizohBrain(parsed.payload || {}).catch(() => ({\n ok: false,\n reply: \"Rhizoh temporary disturbance. Habitat remains stable.\"\n }));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, fallback)));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.BROADCAST_REGISTER) {\n const role = String(parsed.payload?.role || \"\");\n if (role === \"GENESIS_BROADCAST_AGENT\") {\n broadcasterClientId = socket.clientId;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, registered: role })));\n broadcastState();\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.STUDIO_CUE) {\n // Only registered broadcaster can push global cues.\n if (!broadcasterClientId || socket.clientId !== broadcasterClientId) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Only broadcaster agent can emit STUDIO_CUE.\" })));\n return;\n }\n const cue = parsed.payload || {};\n if (typeof cue !== \"object\") {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid STUDIO_CUE payload.\" })));\n return;\n }\n broadcast(createEnvelope(WS_MESSAGE.STUDIO_CUE, cue));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_JOIN_ROOM) {\n const requested = String(parsed.payload?.roomId || \"\").trim();\n const requestedName = String(parsed.payload?.roomName || \"\").trim();\n const roomId = ensureRoom(requested || `room-${Math.random().toString(36).slice(2, 7)}`, requestedName || null);\n spiralState.activeByClient.set(socket.clientId, roomId);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, roomId })));\n broadcastSpiralState();\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_CREATE_CHARACTER) {\n const name = String(parsed.payload?.name || \"\").trim().slice(0, 32) || `AI-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;\n const role = String(parsed.payload?.role || \"agent\").trim().slice(0, 24) || \"agent\";\n const roomId = ensureRoom(\n String(parsed.payload?.roomId || spiralState.activeByClient.get(socket.clientId) || spiralState.rooms[0].id),\n null\n );\n\n // Character creation always maps to an authoritative world agent.\n const result = orchestrator.applyCommand({\n command: COMMAND.SPAWN_AGENT,\n params: { targetCastleId: \"castle-01\" }\n });\n if (!result?.ok || !result?.data?.id) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Character creation failed.\" })));\n return;\n }\n const id = result.data.id;\n spiralState.characters.set(id, { id, name, role, roomId, source: \"spawned\" });\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, characterId: id, roomId })));\n broadcastSpiralState();\n return;\n }\n });\n\n socket.on(\"close\", () => {\n if (socket.clientId === broadcasterClientId) broadcasterClientId = null;\n clientStats.delete(socket.clientId);\n rhizohVoiceLiveSessions.delete(socket);\n spiralState.activeByClient.delete(socket.clientId);\n removeCastleSocialClientFromAllRooms(socket.clientId);\n broadcast(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }));\n broadcastState();\n });\n});\n\nsetInterval(() => {\n void (async () => {\n try {\n const { frame, snapshot, delta, packedDelta } = orchestrator.tick(1 / 20);\n syncSpiralCharactersWithWorld(snapshot);\n broadcast(createEnvelope(WS_MESSAGE.NETWORK_TICK, { frame }));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_TICK, snapshot));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA, delta));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA_PACKED, packedDelta));\n broadcastSpiralState();\n await runAcademyEventTick();\n const executed = await tickAutomations(Date.now());\n for (const a of executed) {\n await addTranscript(a.uid, {\n source: \"planner\",\n eventType: \"automation-executed\",\n text: `Automation executed: ${a.title} (${a.action})`,\n roomId: \"studio-main\",\n meta: { automationId: a.id, action: a.action, deviceId: a.deviceId || \"\" }\n });\n }\n } catch (e) {\n console.error(\"[GATEWAY] world tick / automations loop error:\", String(e?.message || e));\n }\n })();\n}, 50);\n\n(async () => {\n await rhizohRuntime.telemetry.initOpenTelemetry();\n if (String(process.env.CASTLE_GENESIS_DISK_PERSIST ?? \"\").trim() === \"\") {\n process.env.CASTLE_GENESIS_DISK_PERSIST = \"1\";\n }\n const genesisDiskBoot = await hydrateGenesisContinuityPersistenceBootV0();\n if (!genesisDiskBoot.ok) {\n console.warn(\"[GATEWAY] genesis disk hydrate failed; continuing with in-memory genesis continuity\");\n }\n httpServer.listen(PORT, () => {\n startGenesisCanonicalClock();\n installGenesisCheckpointSurfaceGetter(buildGenesisRuntimeSurfacePayloadLive);\n setGenesisContinuityAfterPublishHook((seq) => {\n noteGenesisCheckpointSeqCommitted(seq);\n noteGenesisContinuitySeqAcceptedV0(seq);\n });\n startGenesisContinuityInfraSampler(buildGenesisRuntimeSurfacePayloadLive, 2000);\n logProductionObservatorySurfaceGuardsV0();\n logGatewaySubstrateAuthorityGuardsV0();\n if (isLlmWorkerEnabledV0()) {\n try {\n const workerBoot = initLlmWorkerAtBootV0();\n console.log(\"[GATEWAY] llm worker boot:\", workerBoot);\n } catch (workerErr) {\n console.error(\"[GATEWAY] llm worker boot failed:\", workerErr);\n }\n }\n console.log(`[GATEWAY] ws/http://localhost:${PORT}`);\n console.log(\n `[GATEWAY] genesis continuity: GET ${rhizohRuntime.routes.genesisRuntime} | POST ${rhizohRuntime.routes.genesisIngress} | SSE ${rhizohRuntime.routes.genesisStream} | replay ${rhizohRuntime.routes.genesisReplay} | diff ${rhizohRuntime.routes.genesisReplayDiff} | equiv ${rhizohRuntime.routes.genesisReplayEquivalence} | analytics ${rhizohRuntime.routes.genesisReplayAnalytics} | evolution ${rhizohRuntime.routes.genesisReplayEvolution}`\n );\n if (String(process.env.CASTLE_GENESIS_OBSERVATORY_LIVE_V01 ?? \"\").trim() === \"1\") {\n console.log(\"GENESIS OBSERVATORY LIVE v0.1 — READ ONLY PHASE STARTED\");\n }\n });\n})().catch((e) => {\n console.error(\"[GATEWAY] fatal boot (before or during listen):\", e);\n process.exit(1);\n});\n"}, "f076a594eeae572fcc3cdd3769ee2699dd8fb0b2": {"t": 1785754144.1818116, "n": 168516, "h": "0bb6fc79db0d6bbe", "c": "import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport dotenv from \"dotenv\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconsole.log(\"🔥 [GENESIS_BOOT] entry file:\", __filename);\nconsole.log(\"🔥 [GENESIS_BOOT] node env:\", process.env.NODE_ENV);\nconsole.log(\"🔥 [GENESIS_BOOT] pid:\", process.pid);\nconsole.log(\"🧬 [GENESIS_BOOT] cwd:\", process.cwd());\nconsole.log(\"🧬 [GENESIS_BOOT] import.meta.url:\", import.meta.url);\ndotenv.config({ path: path.join(__dirname, \"..\", \".env\") });\ndotenv.config({ path: path.join(__dirname, \"..\", \".env.local\"), override: true });\n\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport { createServer } from \"node:http\";\nimport { createOrchestrator } from \"@castle/orchestrator\";\nimport { parseTextToCommand } from \"@castle/command-dsl\";\nimport {\n WS_MESSAGE,\n COMMAND,\n createEnvelope,\n createRhizohImmutableEventV0,\n createRhizohPayloadRefV0,\n safeJsonParse,\n validateRhizohVoiceLiveChunkPayloadV0\n} from \"@castle/protocol\";\nimport { queryOpenData } from \"./openData.js\";\nimport { verifyClientToken } from \"./auth.js\";\nimport { runRhizohBrain } from \"./rhizohBrain.js\";\nimport { queryRhizohLlm } from \"./rhizohLlmGateway.js\";\nimport { rhizohGatewayTurn } from \"./rhizohGatewayTurn.js\";\nimport {\n enqueueLlmWorkerTaskAsyncV0,\n getLatestLlmWorkerTaskIdV0,\n getLlmWorkerDebugSnapshotV0,\n getLlmWorkerTaskSnapshotV0,\n initLlmWorkerAtBootV0,\n isLlmWorkerAsyncEnabledV0,\n isLlmWorkerEnabledV0,\n runLlmWorkerTaskV0\n} from \"./llmWorkerRuntime.js\";\nimport {\n RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n runRhizohVoiceTranscribeV3,\n rhizohVoiceTranscribeEnvV3\n} from \"./rhizohVoiceTranscribeV3.js\";\nimport { createHttpCorsPolicy, normalizeHttpOrigin, parseAllowedOriginsFromEnv } from \"./httpCorsPolicyV1.js\";\nimport {\n applyLanguagePropagationToLlmPayloadV1,\n applyLanguagePropagationToVoiceBodyV1,\n buildRhizohLanguagePropagationEchoV1,\n parseRhizohLanguagePropagationV1\n} from \"./rhizohLanguagePropagationV1.js\";\nimport { trimRhizohLlmGatewayPayloadV1 } from \"./rhizohLlmPayloadTrimV1.js\";\nimport {\n meshAppendDelta,\n meshContinuityAggregate,\n meshJoin,\n meshLeave,\n meshReplayFromNodeId,\n meshReplayFromSeq,\n meshSnapshot,\n meshSubscribeSse\n} from \"./presenceRoomMesh.js\";\nimport { runRhizohBrainV2 } from \"./rhizohBrainV2.js\";\nimport {\n listConnections,\n createConnection,\n updateConnection,\n deleteConnection,\n setDefaultConnection,\n resolveConnection\n} from \"./llmConnectionsStore.js\";\nimport { checkHttpRateLimit, getHttpClientIp } from \"./castleHttpRateLimit.js\";\nimport {\n submitAbuseReportV0,\n listModerationQueueV0,\n softBlockUserV0,\n removeSoftBlockV0,\n listSoftBlockedUsersV0\n} from \"./ops/moderationMvpV0.js\";\nimport { listRecentAgentSnapshotsV0 } from \"./ops/agentObservabilityV0.js\";\nimport { readAgentContainmentConfigV0 } from \"./ops/agentContainmentV0.js\";\nimport { readCostContainmentConfigV0, getCostContainmentStatsV0 } from \"./ops/costContainmentV0.js\";\nimport { getGlobalCostLedgerSnapshotV0, getGclBackendStatusV0 } from \"./ops/globalCostLedgerV0.js\";\nimport { evaluateEconomicStrategyL3V0 } from \"./ops/economicStrategyEngineL3V0.js\";\nimport { getPhasedRolloutStatsV0, readPhasedRolloutTierV0 } from \"./ops/phasedRolloutV0.js\";\nimport { buildUnifiedStateNarrativeV0 } from \"./ops/unifiedStateNarrativeV0.js\";\nimport { resolveHardeningTenantIdV0 } from \"./ops/narrativeTenantIsolationV0.js\";\nimport {\n classifyStressResponseV0,\n verifyStressTaxonomyCoverageV0,\n verifyStressConflictResolutionV0\n} from \"./ops/stressResponseTaxonomyV0.js\";\nimport { verifyResolutionStabilityEnvelopeV0 } from \"./ops/resolutionStabilityEnvelopeV0.js\";\nimport { logLlmAccess } from \"./castleLlmAudit.js\";\nimport { appendMemory, listMemories, getMemoryContext, getPersonaGoalMemory, setPersonaGoalMemory, autoCompactMemories } from \"./memoryStore.js\";\nimport { getFirebasePersistence } from \"./firebasePersistence.js\";\nimport { registerAgentIdentity, listAgentIdentities, getAgentIdentity, updateAgentIdentity } from \"./agentIdentityStore.js\";\nimport { queueAcademyEvent, listAcademyEvents, resolveAcademyEvent, runAcademyEventTick } from \"./academyEventEngine.js\";\nimport { enqueuePdfJob, getPdfJob } from \"./pdfPipeline.js\";\nimport {\n createPublishSession,\n listPublishSessions,\n updatePublishSession,\n addTranscript,\n listTranscripts,\n searchMetadata,\n registerDevice,\n listDevices,\n updateDevice,\n appendTelemetry,\n listTelemetry,\n createAutomation,\n listAutomations,\n tickAutomations,\n enqueueCommand,\n listCommands,\n updateCommandStatus,\n registerSocialChannel,\n listSocialChannels\n} from \"./studioOpsStore.js\";\nimport { metrics as infraMetrics } from \"./infra/metrics.js\";\nimport { renderRhizohPrometheusMetrics, rhizohEnterpriseMetrics } from \"./infra/rhizohEnterpriseMetrics.js\";\nimport { scoreHealth } from \"./infra/healthScore.js\";\nimport { getTraceById, listTracesBySession } from \"./infra/traceRegistry.js\";\nimport { canonicalEpistemicSealString, hashAndSignEpistemicSeal, EPISTEMIC_SEAL_SCHEMA } from \"./epistemicSeal.js\";\nimport { persistEpistemicLedgerBatch } from \"./epistemicLedgerStore.js\";\nimport { persistEpistemicForecastBatch } from \"./epistemicForecastStore.js\";\nimport { buildRhizohExternalGroundTruthPayload } from \"./rhizohExternalGroundTruthGateway.js\";\nimport { ingestRhizohExternalLossBatchHttp } from \"./rhizohExternalLossIngestGateway.js\";\nimport {\n ingestRhizohProductOutcomeHttp,\n listRhizohProductOutcomeAggregates,\n verifyRhizohOutcomeHmac,\n verifyRhizohOutcomeSourceToken\n} from \"./rhizohProductOutcomeIngestGateway.js\";\nimport { initRhizoh } from \"./rhizohProductionBootstrap.js\";\nimport { handleAcademicObservatoryExportGetV0 } from \"./rhizoh/academicObservatoryHttpV0.js\";\nimport {\n handleLiveSportsBundleGetV0,\n handleLiveNewsHeadlinesGetV0,\n handleLiveWorldFeedGetV0\n} from \"./rhizoh/liveWorldFeedHttpV0.js\";\nimport {\n buildGenesisRuntimeSurfacePayload,\n recordGenesisEpistemicLedgerPersisted,\n recordGenesisEpistemicSealIssued,\n startGenesisCanonicalClock\n} from \"./genesisRuntimeSurfaceV0.js\";\nimport { registerGenesisSseClient, getGenesisContinuitySeq, setGenesisContinuityAfterPublishHook } from \"./genesisContinuityStreamHubV0.js\";\nimport { computeGenesisReplayFingerprintV0 } from \"./genesisReplayFingerprintV0.js\";\nimport { buildGenesisGatewayCapabilitiesV0 } from \"./genesisGatewayCapabilitiesV0.js\";\nimport {\n installGenesisCheckpointSurfaceGetter,\n noteGenesisCheckpointSeqCommitted,\n getLatestGenesisSignedCheckpoint\n} from \"./genesisContinuityCheckpointV0.js\";\nimport { hydrateGenesisContinuityPersistenceBootV0 } from \"./genesisContinuityHydrateBootV0.js\";\nimport { startGenesisContinuityInfraSampler } from \"./genesisContinuityInfraSamplerV0.js\";\nimport {\n genesisCheckpointQueryBySeqV0,\n genesisCheckpointQueryRangeV0,\n genesisCheckpointQueryLineageV0\n} from \"./genesisCheckpointQueryV0.js\";\nimport {\n genesisContinuityEventArchiveEnabled,\n queryGenesisContinuityEventArchiveV0,\n GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION\n} from \"./genesisContinuityEventArchiveV0.js\";\nimport { resolveGenesisReplayRouterV1, GENESIS_REPLAY_ROUTER_SCHEMA } from \"./genesisReplayRouterV1.js\";\nimport {\n computeGenesisReplayTemporalDiffV1,\n GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA\n} from \"./genesisReplayTemporalDiffV1.js\";\nimport { compareGenesisReplayEquivalenceV1 } from \"./genesisReplayEquivalenceV1.js\";\nimport { computeGenesisReplayAnalyticsV1 } from \"./genesisReplayAnalyticsV1.js\";\nimport { computeGenesisReplayEvolutionV1 } from \"./genesisReplayEvolutionV1.js\";\nimport { handleStripeCheckoutCreate, handleStripeMembershipWebhook } from \"./stripeWebhookMembership.js\";\nimport { buildRealityHealthPayload } from \"./edgeRealityConsistencyV1.js\";\nimport {\n assertWalPeerSocketAuthorityV0,\n buildGatewaySubstrateAuthoritySnapshotV0,\n logGatewaySubstrateAuthorityGuardsV0,\n validateWalPeerFeedSignatureV0\n} from \"./gatewaySubstrateAuthorityV0.js\";\nimport {\n buildSubstrateOperationalSnapshotV0,\n recordWalPeerRejectV0,\n renderSubstratePrometheusMetricsV0\n} from \"./infra/substrateOperationalMetrics.js\";\nimport { ingestClientSubstrateHealthV0 } from \"./rhizohSubstrateHealthIngestV0.js\";\nimport { recordSubstrateOtelEventV0 } from \"./infra/opentelemetrySubstrateV0.js\";\nimport {\n GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n getGenesisWorldObservationIngressStatusV0,\n ingestGenesisWorldObservationV0\n} from \"./genesisContinuityClientIngressV0.js\";\nimport {\n getGenesisContinuitySeqAuditV0,\n noteGenesisContinuitySeqAcceptedV0\n} from \"./genesisContinuitySeqGapV0.js\";\n\n/** Render / Fly / Railway: `PORT` — yoksa `CASTLE_GATEWAY_PORT` — yoksa 8090. */\nconst PORT =\n Number(process.env.PORT) ||\n Number(process.env.CASTLE_GATEWAY_PORT) ||\n 8090;\n/** Spiral / world WebSocket server — set after `wss` init; used by genesis runtime surface. */\nlet spiralWssForGenesis = null;\nconst MAX_MESSAGE_BYTES = Number(process.env.CASTLE_MAX_MESSAGE_BYTES || 32 * 1024);\nconst REQUIRED_GATEWAY_TOKEN = process.env.CASTLE_GATEWAY_TOKEN || \"\";\nconst ALLOWED_ORIGINS = parseAllowedOriginsFromEnv().fromList;\nconst httpCors = createHttpCorsPolicy();\n\n/** @returns {string|null} `Access-Control-Allow-Origin`; null = browser CORS fail. */\nfunction accessControlAllowOriginValue(req) {\n return httpCors.accessControlAllowOriginValue(req);\n}\n\nfunction applyHttpCorsHeaders(req, res) {\n return httpCors.applyHttpCorsHeaders(req, res);\n}\nconst REQUIRE_AUTH = process.env.CASTLE_REQUIRE_AUTH === \"true\";\nconst ALLOW_DEV_ANON = process.env.CASTLE_ALLOW_DEV_ANON !== \"false\";\nconst ALLOW_DEV_HTTP_UID = process.env.CASTLE_ALLOW_DEV_HTTP_UID !== \"false\";\n/** Presence mesh: allow `X-Castle-Guest-Id` when Firebase auth absent (default on). Set `false` to require auth only. */\nconst ALLOW_MESH_GUEST = process.env.CASTLE_ALLOW_MESH_GUEST !== \"false\";\nconst SOCIAL_RETRY_MAX = Math.max(0, Math.min(5, Number(process.env.CASTLE_SOCIAL_RETRY_MAX || 2)));\nconst SOCIAL_RETRY_BASE_MS = Math.max(100, Number(process.env.CASTLE_SOCIAL_RETRY_BASE_MS || 700));\nconst TELEGRAM_BOT_TOKEN = process.env.CASTLE_TELEGRAM_BOT_TOKEN || \"\";\nconst WHATSAPP_TOKEN = process.env.CASTLE_WHATSAPP_TOKEN || \"\";\nconst WHATSAPP_PHONE_NUMBER_ID = process.env.CASTLE_WHATSAPP_PHONE_NUMBER_ID || \"\";\nconst RL_RHIZOH_LLM_PER_MIN = Math.max(5, Number(process.env.CASTLE_RL_RHIZOH_LLM_PER_MIN || 40));\nconst RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN || 30)\n);\nconst RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN || 120));\nconst RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN = Math.max(\n 10,\n Number(process.env.CASTLE_RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN || 60)\n);\nconst RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN = Math.max(8, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN || 48));\nconst RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN = Math.max(4, Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN || 24));\nconst RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN || 18)\n);\nconst RL_EPISTEMIC_SEAL_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_EPISTEMIC_SEAL_PER_MIN || 120));\nconst RL_EPISTEMIC_LOG_PER_MIN = Math.max(20, Number(process.env.CASTLE_RL_EPISTEMIC_LOG_PER_MIN || 240));\nconst EPISTEMIC_SEAL_SECRET = String(\n process.env.CASTLE_EPISTEMIC_SEAL_SECRET || process.env.CASTLE_GATEWAY_TOKEN || \"\"\n).trim();\nconst RL_LLM_CONN_TEST_PER_MIN = Math.max(2, Number(process.env.CASTLE_RL_LLM_CONNECTION_TEST_PER_MIN || 8));\n\n/** One-shot production warnings: CORS surface, auth toggle, dev bypass flags (observatory deploy hygiene). */\nfunction logProductionObservatorySurfaceGuardsV0() {\n if (String(process.env.NODE_ENV || \"\").toLowerCase() !== \"production\") return;\n const tag = \"[GATEWAY_PROD_GUARD]\";\n const primaryCors = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n const corsListEmpty = ALLOWED_ORIGINS.length === 0;\n if (corsListEmpty && !primaryCors) {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty and CASTLE_HTTP_CORS_ORIGIN empty — production still merges RHIZOH_BROWSER_ORIGINS_V1 defaults; set env for preview/staging hosts`\n );\n } else if (corsListEmpty && primaryCors && primaryCors !== \"*\") {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty — only CASTLE_HTTP_CORS_ORIGIN set; add web.app + firebaseapp.com (and previews) to CASTLE_ALLOWED_ORIGINS if clients use alternate hostnames`\n );\n }\n if (primaryCors && /localhost|127\\.0\\.0\\.1/i.test(primaryCors)) {\n console.warn(`${tag} CORS: CASTLE_HTTP_CORS_ORIGIN is localhost while NODE_ENV=production`);\n }\n if (REQUIRE_AUTH) {\n console.warn(\n `${tag} AUTH: CASTLE_REQUIRE_AUTH=true — clients must send a valid gateway credential (otherwise silent 401 on replay/SSE); EventSource cannot set custom headers unless proxied`\n );\n if (REQUIRED_GATEWAY_TOKEN.length < 16) {\n console.warn(`${tag} AUTH: CASTLE_GATEWAY_TOKEN missing or short while require-auth enabled`);\n }\n }\n if (ALLOW_DEV_ANON) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_ANON is not \"false\" — dev anonymous path may be live in production (set CASTLE_ALLOW_DEV_ANON=false)`\n );\n }\n if (ALLOW_DEV_HTTP_UID) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_HTTP_UID is not \"false\" — X-Castle-Dev-Uid bypass risk (set CASTLE_ALLOW_DEV_HTTP_UID=false)`\n );\n }\n}\n\nconst rhizohRuntime = initRhizoh();\n\n/** İstemci: llmKeySource veya keySource — env | user_connection | auto */\nfunction normalizeClientLlmKeySource(raw) {\n const s = String(raw ?? \"\").trim().toLowerCase();\n if (s === \"env\" || s === \"server\") return \"env\";\n if (s === \"user_connection\" || s === \"connection\" || s === \"user\") return \"user_connection\";\n if (s === \"auto\" || s === \"\") return \"auto\";\n return null;\n}\n\nfunction jsonReplacerSafeV0(_key, value) {\n if (typeof value === \"bigint\") return Number(value);\n return value;\n}\n\nfunction sendJson(res, statusCode, payload) {\n res.writeHead(statusCode, { \"Content-Type\": \"application/json; charset=utf-8\" });\n try {\n res.end(JSON.stringify(payload, jsonReplacerSafeV0));\n } catch (error) {\n res.end(\n JSON.stringify({\n ok: false,\n error: \"response_serialize_failed\",\n detail: String(error?.message || error)\n })\n );\n }\n}\n\nfunction logRhizohHealth(stage, detail = {}) {\n try {\n console.info(`[CASTLE_${String(stage || \"unknown\")}]`, detail && typeof detail === \"object\" ? detail : {});\n } catch {\n /* noop */\n }\n}\n\nfunction renderInfraMetricsProm() {\n const lines = [\n `castle_gateway_events_processed_total ${infraMetrics.eventsProcessed}`,\n `castle_gateway_duplicate_reject_total ${infraMetrics.duplicateRejects}`,\n `castle_gateway_enqueue_latency_ms ${infraMetrics.enqueueLatencyMs}`,\n `castle_gateway_queue_depth ${infraMetrics.queueDepth}`,\n `castle_gateway_queue_lag_seconds ${(Number(infraMetrics.queueLag || 0) / 1000).toFixed(3)}`,\n `castle_gateway_errors_total ${infraMetrics.errors}`\n ];\n const buckets = infraMetrics.enqueueLatencyBuckets || {};\n for (const k of Object.keys(buckets)) {\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"${k}\"} ${buckets[k]}`);\n }\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"+Inf\"} ${infraMetrics.eventsProcessed}`);\n if (process.env.CASTLE_SUBSTRATE_METRICS !== \"0\") {\n lines.push(renderSubstratePrometheusMetricsV0().trimEnd());\n }\n return lines.join(\"\\n\");\n}\n\nasync function fetchJsonWithTimeout(url, timeoutMs = 1200) {\n const ctl = new AbortController();\n const tid = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const res = await fetch(url, { signal: ctl.signal });\n if (!res.ok) return null;\n return await res.json();\n } catch {\n return null;\n } finally {\n clearTimeout(tid);\n }\n}\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction getHttpPathname(req) {\n let p = String(req.url || \"\").split(\"?\")[0];\n /* Reverse proxies / clients sometimes append `/` — avoid silent 404 on e.g. `/rhizoh/genesis/stream/`. */\n while (p.length > 1 && p.endsWith(\"/\")) {\n p = p.slice(0, -1);\n }\n return p;\n}\n\nfunction rhizohLlmEnvConfigured() {\n const p = String(process.env.CASTLE_LLM_PROVIDER || \"openai\").toLowerCase();\n const key =\n p === \"anthropic\"\n ? process.env.ANTHROPIC_API_KEY\n : p === \"gemini\"\n ? process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY\n : p === \"xai\"\n ? process.env.XAI_API_KEY\n : p === \"deepseek\"\n ? process.env.DEEPSEEK_API_KEY\n : p === \"mistral\"\n ? process.env.MISTRAL_API_KEY\n : p === \"openrouter\"\n ? process.env.OPENROUTER_API_KEY\n : process.env.OPENAI_API_KEY;\n return !!String(key || \"\").trim();\n}\n\n/**\n * @param {import(\"firebase-admin/firestore\").Firestore | null} db\n * @param {number} timeoutMs\n */\nasync function probeFirestoreReachable(db, timeoutMs = 2000) {\n if (!db) return { ok: false, reason: \"no_db\" };\n const timeout = sleep(timeoutMs).then(() => ({ ok: false, reason: \"timeout\" }));\n const work = db\n .collection(\"_castle_gateway_health\")\n .doc(\"ping\")\n .get()\n .then(() => ({ ok: true, reason: \"reachable\" }))\n .catch((e) => ({ ok: false, reason: String(e?.message || e || \"firestore_error\") }));\n return Promise.race([work, timeout]);\n}\n\nfunction stripHtml(text = \"\") {\n return String(text)\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \" \")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \" \")\n .replace(/<[^>]+>/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction haversineMeters(aLat, aLon, bLat, bLon) {\n const R = 6371000;\n const dLat = ((bLat - aLat) * Math.PI) / 180;\n const dLon = ((bLon - aLon) * Math.PI) / 180;\n const sa = Math.sin(dLat / 2);\n const sb = Math.sin(dLon / 2);\n const x = sa * sa + Math.cos((aLat * Math.PI) / 180) * Math.cos((bLat * Math.PI) / 180) * sb * sb;\n return 2 * R * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));\n}\n\nasync function checkPublishTargetHealth(protocol, target) {\n const p = String(protocol || \"\").toUpperCase();\n const t = String(target || \"\").trim();\n if (!t) return { ok: false, reason: \"target_missing\", shouldReconnect: false };\n if (p === \"RTMP\") {\n const ok = t.startsWith(\"rtmp://\") || t.startsWith(\"rtmps://\");\n return { ok, reason: ok ? \"rtmp_target_format_ok\" : \"invalid_rtmp_target\", shouldReconnect: !ok, reconnectInSec: 15 };\n }\n if (p === \"WHIP\" || p === \"SFU\") {\n try {\n const res = await fetch(t, { method: \"OPTIONS\" });\n const ok = res.status < 500;\n return { ok, reason: `http_${res.status}`, shouldReconnect: !ok, reconnectInSec: 8 };\n } catch {\n return { ok: false, reason: \"network_unreachable\", shouldReconnect: true, reconnectInSec: 8 };\n }\n }\n return { ok: true, reason: \"unchecked_protocol\", shouldReconnect: false };\n}\n\nasync function fetchTextFromUrl(url) {\n const res = await fetch(url, { method: \"GET\" });\n if (!res.ok) throw new Error(`fetch_${res.status}`);\n const ct = String(res.headers.get(\"content-type\") || \"\");\n if (ct.includes(\"application/pdf\")) throw new Error(\"pdf_binary_not_supported_directly\");\n const body = await res.text();\n return stripHtml(body).slice(0, 14000);\n}\n\nfunction buildTaskActionGraph(task = \"\", context = {}) {\n const t = String(task || \"\").toLowerCase();\n if (t.includes(\"dinner\") || t.includes(\"akşam yemeği\") || t.includes(\"aksam yemegi\")) {\n return {\n taskTitle: \"Dinner Prep Planner\",\n nodes: [\n { id: \"n1\", title: \"Kitchen preheat\", action: \"smart-oven.preheat\", params: { temperatureC: 190, mode: \"fan\" } },\n { id: \"n2\", title: \"Lighting comfort\", action: \"smart-light.set_scene\", params: { scene: \"warm_dinner\" } },\n { id: \"n3\", title: \"Fridge inventory check\", action: \"smart-fridge.inventory_scan\", params: {} },\n { id: \"n4\", title: \"Reminder notification\", action: \"smart-watch.notify\", params: { text: \"Dinner prep started.\" } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" },\n { from: \"n3\", to: \"n4\" }\n ]\n };\n }\n return {\n taskTitle: task || \"General Planner\",\n nodes: [\n { id: \"n1\", title: \"Safety check\", action: \"device.safety_check\", params: { context } },\n { id: \"n2\", title: \"Primary action\", action: \"device.execute_task\", params: { task } },\n { id: \"n3\", title: \"Report\", action: \"studio.timeline_note\", params: { task } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" }\n ]\n };\n}\n\nfunction buildEthicsPrograms() {\n return {\n policyVersion: \"v1\",\n tracks: [\n {\n id: \"elderly-care-assist\",\n audience: \"elderly\",\n title: \"Elderly Assistance Track\",\n principles: [\"human-in-the-loop\", \"consent-first\", \"low-friction alerts\"],\n modules: [\"medication-reminder\", \"fall-risk-check\", \"family-notify\", \"daily-companion-brief\"]\n },\n {\n id: \"kids-safe-learning\",\n audience: \"children\",\n title: \"Kids Safe Learning Track\",\n principles: [\"guardian-consent\", \"age-appropriate-language\", \"content-filtering\"],\n modules: [\"study-coach\", \"screen-time-balance\", \"safe-social-share\", \"emergency-contact\"]\n }\n ]\n };\n}\n\nasync function deliverViaTelegram(channel, text) {\n const chatId = String(channel.endpoint || \"\").trim();\n if (!TELEGRAM_BOT_TOKEN) throw new Error(\"telegram_token_missing\");\n if (!chatId) throw new Error(\"telegram_chat_id_missing\");\n const url = `https://api.telegram.org/bot${encodeURIComponent(TELEGRAM_BOT_TOKEN)}/sendMessage`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ chat_id: chatId, text })\n });\n if (!res.ok) throw new Error(`telegram_http_${res.status}`);\n return { provider: \"telegram\", status: res.status };\n}\n\nasync function deliverViaWhatsapp(channel, text) {\n const to = String(channel.endpoint || \"\").trim();\n if (!WHATSAPP_TOKEN) throw new Error(\"whatsapp_token_missing\");\n if (!WHATSAPP_PHONE_NUMBER_ID) throw new Error(\"whatsapp_phone_number_id_missing\");\n if (!to) throw new Error(\"whatsapp_to_missing\");\n const url = `https://graph.facebook.com/v22.0/${encodeURIComponent(WHATSAPP_PHONE_NUMBER_ID)}/messages`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${WHATSAPP_TOKEN}` },\n body: JSON.stringify({ messaging_product: \"whatsapp\", to, type: \"text\", text: { body: text } })\n });\n if (!res.ok) throw new Error(`whatsapp_http_${res.status}`);\n return { provider: \"whatsapp\", status: res.status };\n}\n\nasync function deliverViaWebhook(channel, text) {\n const endpoint = String(channel.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"webhook_endpoint_missing\");\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text, platform: channel.platform || \"webhook\", ts: Date.now() })\n });\n if (!res.ok) throw new Error(`webhook_http_${res.status}`);\n return { provider: \"webhook\", status: res.status };\n}\n\nasync function deliverSocialMessage(channel, text) {\n const platform = String(channel.platform || \"webhook\").toLowerCase();\n if (platform === \"telegram\") return deliverViaTelegram(channel, text);\n if (platform === \"whatsapp\") return deliverViaWhatsapp(channel, text);\n return deliverViaWebhook(channel, text);\n}\n\nasync function deliverSocialWithRetry(channel, text) {\n let attempt = 0;\n let lastError = null;\n while (attempt <= SOCIAL_RETRY_MAX) {\n try {\n const result = await deliverSocialMessage(channel, text);\n return { ok: true, attempt: attempt + 1, ...result };\n } catch (error) {\n lastError = error;\n if (attempt >= SOCIAL_RETRY_MAX) break;\n const jitter = Math.floor(Math.random() * 90);\n await sleep(SOCIAL_RETRY_BASE_MS * Math.pow(2, attempt) + jitter);\n }\n attempt += 1;\n }\n return { ok: false, attempt: attempt + 1, error: String(lastError?.message || \"delivery_failed\") };\n}\n\nasync function dispatchCommandToAdapter(device, command) {\n const adapter = String(command?.adapter || device?.adapter || \"websocket\").toLowerCase();\n const endpoint = String(device?.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"device_endpoint_missing\");\n if (adapter === \"mqtt\") {\n // MQTT delivery is proxied via a bridge endpoint (HTTP webhook style).\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n type: \"mqtt-command\",\n topic: `castle/devices/${device.id}/commands`,\n action: command.action,\n params: command.params || {}\n })\n });\n if (!res.ok) throw new Error(`mqtt_bridge_http_${res.status}`);\n return { ok: true, adapter, via: \"http-bridge\", status: res.status };\n }\n if (!endpoint.startsWith(\"ws://\") && !endpoint.startsWith(\"wss://\")) throw new Error(\"invalid_websocket_endpoint\");\n const sendResult = await new Promise((resolve, reject) => {\n const ws = new WebSocket(endpoint);\n const timer = setTimeout(() => {\n try {\n ws.close();\n } catch {\n /* noop */\n }\n reject(new Error(\"websocket_timeout\"));\n }, 3000);\n ws.on(\"open\", () => {\n ws.send(\n JSON.stringify({\n type: \"castle-command\",\n commandId: command.id,\n deviceId: command.deviceId,\n action: command.action,\n params: command.params || {}\n }),\n (err) => {\n clearTimeout(timer);\n ws.close();\n if (err) reject(new Error(\"websocket_send_failed\"));\n else resolve({ ok: true, adapter: \"websocket\", via: \"ws\" });\n }\n );\n });\n ws.on(\"error\", () => {\n clearTimeout(timer);\n reject(new Error(\"websocket_connect_failed\"));\n });\n });\n return sendResult;\n}\n\nfunction readHttpJson(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => {\n if (!buf) return resolve({});\n const payload = safeJsonParse(buf);\n if (!payload || typeof payload !== \"object\") return reject(new Error(\"invalid_json_payload\"));\n resolve(payload);\n });\n req.on(\"error\", reject);\n });\n}\n\n/** Ham gövde (HMAC doğrulaması için). */\nfunction readHttpBodyText(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => resolve(buf));\n req.on(\"error\", reject);\n });\n}\n\nfunction maskOpaqueId(value) {\n const t = String(value || \"\");\n if (!t) return \"(empty)\";\n if (t.length <= 8) return \"****\";\n return `${t.slice(0, 4)}…${t.slice(-2)}`;\n}\n\nfunction readGatewayHttpToken(req) {\n const x = String(req.headers?.[\"x-castle-gateway-token\"] || \"\").trim();\n if (x) return x;\n const auth = String(req.headers?.authorization || \"\");\n const m = /^Bearer\\s+(.+)$/i.exec(auth);\n return m ? String(m[1] || \"\").trim() : \"\";\n}\n\nasync function resolveHttpUser(req) {\n const authResult = await verifyClientToken(req);\n if (authResult?.ok && authResult.user?.uid) {\n return { ok: true, uid: String(authResult.user.uid), mode: authResult.kind || \"auth\" };\n }\n if (ALLOW_DEV_ANON && ALLOW_DEV_HTTP_UID) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) return { ok: true, uid: `dev-${devUid.slice(0, 64)}`, mode: \"dev-header\" };\n }\n return { ok: false, reason: authResult?.reason || \"auth_required\" };\n}\n\n/**\n * Epistemic ledger ingest — Firebase uid OR valid transport token + X-Castle-Dev-Uid.\n * Prod disables CASTLE_ALLOW_DEV_HTTP_UID; LLM may still work while telemetry 401'd without this.\n */\nasync function resolveEpistemicIngestActor(req) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok && tok === REQUIRED_GATEWAY_TOKEN) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) {\n return { ok: true, uid: `gw-telemetry-${devUid.slice(0, 64)}`, mode: \"gateway-token-dev-uid\" };\n }\n return { ok: false, reason: \"dev_uid_required\" };\n }\n if (tok) {\n return { ok: false, reason: \"gateway_token_mismatch\" };\n }\n }\n const auth = await resolveHttpUser(req);\n if (auth.ok) return auth;\n return { ok: false, reason: auth.reason || \"auth_required\" };\n}\n\n/** Mesh actor: Firebase uid, dev header, or `guest:<uuid>` via header / `guestId` query (SSE cannot set headers). */\nasync function resolveMeshActor(req) {\n const auth = await resolveHttpUser(req);\n if (auth.ok) return { ok: true, clientUid: auth.uid, mode: auth.mode };\n if (ALLOW_MESH_GUEST) {\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const gid = String(req.headers?.[\"x-castle-guest-id\"] || q.searchParams.get(\"guestId\") || \"\").trim();\n if (gid) {\n const normalized = gid.startsWith(\"guest:\") ? gid.slice(0, 180) : `guest:${gid.slice(0, 160)}`;\n return { ok: true, clientUid: normalized, mode: \"guest-header\" };\n }\n }\n return { ok: false, reason: \"mesh_auth_required\" };\n}\n\n/** Live genesis runtime JSON (GET /rhizoh/genesis/runtime + SSE infra sampler). */\nasync function buildGenesisRuntimeSurfacePayloadLive() {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n let spiralWsActive = 0;\n if (spiralWssForGenesis) {\n for (const c of spiralWssForGenesis.clients) {\n if (c.readyState === 1) spiralWsActive += 1;\n }\n }\n const base = buildGenesisRuntimeSurfacePayload({\n infraMetrics,\n scoredHealth: scored,\n rhizohEnterpriseMetrics,\n mesh: meshContinuityAggregate(),\n workerHealth: worker,\n port: PORT,\n spiralWebSocketClientsActive: spiralWsActive\n });\n return {\n ...base,\n replayFingerprint: computeGenesisReplayFingerprintV0(base),\n gatewayCapabilities: buildGenesisGatewayCapabilitiesV0(),\n genesisStream: {\n schema: \"castle.genesis.stream_cursor.v0\",\n lastAcceptedSeq: getGenesisContinuitySeq(),\n clientIngress: {\n schema: GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n contract: \"castle.world_observation.ingress_envelope.v1\",\n postPath: rhizohRuntime.routes.genesisIngress,\n closure: getGenesisWorldObservationIngressStatusV0()\n },\n seqContinuity: getGenesisContinuitySeqAuditV0(),\n eventArchive: genesisContinuityEventArchiveEnabled()\n ? {\n enabled: true,\n projection: GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION,\n queryPath: rhizohRuntime.routes.genesisContinuityEvents\n }\n : { enabled: false },\n replayRouter: {\n schema: GENESIS_REPLAY_ROUTER_SCHEMA,\n queryPath: rhizohRuntime.routes.genesisReplay,\n queryHint: \"from=&to= or range=from-to; optional type=&checkpoints=0\",\n temporalDiffPath: rhizohRuntime.routes.genesisReplayDiff,\n temporalDiffSchema: GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA,\n equivalencePath: rhizohRuntime.routes.genesisReplayEquivalence,\n analyticsPath: rhizohRuntime.routes.genesisReplayAnalytics,\n evolutionPath: rhizohRuntime.routes.genesisReplayEvolution\n }\n }\n };\n}\n\nconst httpServer = createServer(async (req, res) => {\n const corsAllow = applyHttpCorsHeaders(req, res);\n if (req.method === \"OPTIONS\") {\n if (!corsAllow && req.headers?.origin) {\n console.warn(\"[GATEWAY_CORS] preflight rejected for origin:\", req.headers.origin);\n }\n res.writeHead(corsAllow ? 204 : 403);\n res.end();\n return;\n }\n\n const pathname = getHttpPathname(req);\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/genesis/__ping\") {\n sendJson(res, 200, {\n ok: true,\n pid: process.pid,\n entry: __filename,\n time: Date.now(),\n genesis: true\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/epistemic/__probe\") {\n sendJson(res, 200, {\n ok: true,\n epistemicIngestActorV2: true,\n routes: {\n seal: rhizohRuntime.routes.epistemicSeal,\n logsBatch: rhizohRuntime.routes.epistemicLogsBatch\n },\n hasGatewayToken: Boolean(REQUIRED_GATEWAY_TOKEN),\n gatewayTokenLen: REQUIRED_GATEWAY_TOKEN ? REQUIRED_GATEWAY_TOKEN.length : 0\n });\n return;\n }\n\n if (\n req.method === \"GET\" &&\n (pathname === \"/rhizoh/live/world-feed\" ||\n pathname === \"/rhizoh/live/sports-bundle\" ||\n pathname === \"/rhizoh/live/news-headlines\")\n ) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_live_world_feed:${ip}`, RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const out =\n pathname === \"/rhizoh/live/sports-bundle\"\n ? await handleLiveSportsBundleGetV0()\n : pathname === \"/rhizoh/live/news-headlines\"\n ? await handleLiveNewsHeadlinesGetV0(req)\n : await handleLiveWorldFeedGetV0(req);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/webhooks/stripe\") {\n await handleStripeMembershipWebhook(req, res);\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/billing/stripe/checkout\") {\n await handleStripeCheckoutCreate(req, res, { readHttpJson, resolveHttpUser, sendJson });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/live\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, live: false, dns: true, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n sendJson(res, 200, { ok: true, live: true, dns: true, service: \"castle-gateway\", ts: Date.now() });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/cors\") {\n const reqOrigin = normalizeHttpOrigin(req.headers?.origin);\n const allowOrigin = accessControlAllowOriginValue(req);\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n requestOrigin: reqOrigin || null,\n allowed: Boolean(allowOrigin),\n allowOrigin: allowOrigin || null,\n allowSetSize: httpCors.allowSet.size\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/ready\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, ready: false, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n try {\n const persistence = getFirebasePersistence();\n const llmProbe = rhizohLlmEnvConfigured();\n sendJson(res, 200, {\n ok: true,\n ready: true,\n dns: true,\n persistence: persistence.mode,\n service: \"castle-gateway\",\n gateway: \"ok\",\n llm: llmProbe ? \"ok\" : \"degraded\",\n spine: llmProbe ? \"ok\" : \"partial\"\n });\n } catch (e) {\n sendJson(res, 503, { ok: false, ready: false, reason: String(e?.message || e), service: \"castle-gateway\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/reality\") {\n const payload = buildRealityHealthPayload();\n sendJson(res, payload.ok ? 200 : 503, payload);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/substrate\") {\n const authority = buildGatewaySubstrateAuthoritySnapshotV0();\n const operational = buildSubstrateOperationalSnapshotV0();\n const drift = buildRealityHealthPayload();\n const ok = Boolean(authority.productionAuthoritySatisfied) && drift.ok;\n sendJson(res, ok ? 200 : 503, {\n ok,\n service: \"castle-gateway\",\n ts: Date.now(),\n authority,\n operational,\n drift: { ok: drift.ok, errors: drift.drift?.errors, warnings: drift.drift?.warnings }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/substrate/health\") {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const body = await readHttpJson(req);\n const uid = (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestClientSubstrateHealthV0(body, { uid, clientId: uid });\n if (!result.ok) {\n sendJson(res, 400, result);\n return;\n }\n recordSubstrateOtelEventV0(\"client_health_ingest\", { uid: String(uid).slice(0, 32) });\n sendJson(res, 200, result);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/deps\") {\n const t0 = Date.now();\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, {\n ok: false,\n dns: true,\n llm: false,\n firestore: false,\n phase: \"maintenance\",\n latencyMs: Date.now() - t0,\n service: \"castle-gateway\"\n });\n return;\n }\n const persistence = getFirebasePersistence();\n let firestoreOk = persistence.mode !== \"firebase\";\n let firestoreDetail = persistence.mode === \"file\" ? \"file_mode\" : \"not_applicable\";\n if (persistence.mode === \"firebase\" && persistence.db) {\n const pr = await probeFirestoreReachable(persistence.db, 2000);\n firestoreOk = pr.ok;\n firestoreDetail = pr.reason || (pr.ok ? \"reachable\" : \"fail\");\n } else if (persistence.mode === \"firebase\" && !persistence.db) {\n firestoreOk = false;\n firestoreDetail = \"firebase_not_initialized\";\n }\n const llmOk = rhizohLlmEnvConfigured();\n const overallOk = llmOk && firestoreOk;\n const latencyMs = Date.now() - t0;\n sendJson(res, 200, {\n ok: overallOk,\n dns: true,\n llm: llmOk,\n firestore: persistence.mode === \"firebase\" ? firestoreOk : false,\n persistence: persistence.mode,\n firestoreDetail,\n latencyMs,\n service: \"castle-gateway\",\n wsPort: PORT\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health\") {\n const persistence = getFirebasePersistence();\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n wsPort: PORT,\n persistence: persistence.mode,\n presenceMesh: \"sse\"\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/health\") {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n sendJson(res, 200, {\n ok: true,\n role: \"gateway\",\n queueLagMs: infraMetrics.queueLag,\n queueDepth: infraMetrics.queueDepth,\n errors: infraMetrics.errors,\n status: scored.status,\n score: scored.score,\n reasons: scored.reasons,\n worker: worker\n ? {\n status: worker.status,\n score: worker.score,\n reasons: worker.reasons\n }\n : null,\n timestamp: Date.now()\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisRuntime) {\n const payload = await buildGenesisRuntimeSurfacePayloadLive();\n sendJson(res, 200, payload);\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.genesisIngress) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const actor = await resolveMeshActor(req);\n const clientId = actor.ok ? actor.clientUid : (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestGenesisWorldObservationV0(body, { clientId });\n if (!result.ok) {\n const status =\n result.error === \"rate_limited\" || result.error === \"agent_spoke_throttled\"\n ? 429\n : result.error === \"payload_too_large\"\n ? 413\n : 400;\n sendJson(res, status, result);\n return;\n }\n sendJson(res, result.idempotent ? 200 : 202, result);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisStream) {\n const corsOrigin = accessControlAllowOriginValue(req);\n if (!corsOrigin && String(process.env.NODE_ENV || \"\").toLowerCase() === \"production\") {\n const og = String(req.headers?.origin || \"\").trim();\n if (og) {\n console.warn(\"[GATEWAY] genesis SSE: missing Access-Control-Allow-Origin for browser Origin (CORS whitelist mismatch):\", og);\n }\n }\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n res.write(\"retry: 3000\\n\\n\");\n res.write(\": genesis continuity stream\\n\\n\");\n res.write(\"event: genesis\\n\");\n res.write(`data: {\"booted\":true,\"pid\":${process.pid}}\\n\\n`);\n registerGenesisSseClient(res, req);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLatest) {\n const cp = getLatestGenesisSignedCheckpoint();\n if (!cp) {\n sendJson(res, 404, { ok: false, error: \"no_checkpoint_yet\", hint: \"wait_for_seq_interval_or_configure_signing_secret\" });\n return;\n }\n sendJson(res, 200, { ok: true, checkpoint: cp });\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(rhizohRuntime.routes.genesisCheckpointBySeqPrefix)) {\n const tail = pathname.slice(rhizohRuntime.routes.genesisCheckpointBySeqPrefix.length).split(\"/\")[0];\n const seq = parseInt(String(tail || \"\").trim(), 10);\n const out = await genesisCheckpointQueryBySeqV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"checkpoint_not_found\") {\n sendJson(res, 404, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointRange) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryRangeV0(from, to);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"range_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLineage) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const seq = parseInt(String(u.searchParams.get(\"seq\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryLineageV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"lineage_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisContinuityEvents) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const limit = parseInt(String(u.searchParams.get(\"limit\") || \"\").trim(), 10);\n const out = await queryGenesisContinuityEventArchiveV0(from, to, type, limit);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\" || out.error === \"event_archive_disabled\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"archive_file_too_large\") {\n sendJson(res, 413, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplay) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await resolveGenesisReplayRouterV1({\n from: fromSeq,\n to: toSeq,\n type,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayDiff) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const out = await computeGenesisReplayTemporalDiffV1({ from: fromSeq, to: toSeq, type });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEquivalence) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from1 = parseInt(String(u.searchParams.get(\"from1\") || \"\").trim(), 10);\n const to1 = parseInt(String(u.searchParams.get(\"to1\") || \"\").trim(), 10);\n const from2 = parseInt(String(u.searchParams.get(\"from2\") || \"\").trim(), 10);\n const to2 = parseInt(String(u.searchParams.get(\"to2\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await compareGenesisReplayEquivalenceV1({\n from1,\n to1,\n from2,\n to2,\n type,\n includeCheckpoints\n });\n if (\n !out.ok &&\n (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")\n ) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayAnalytics) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayAnalyticsV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEvolution) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const collapseWindows = parseInt(String(u.searchParams.get(\"collapseWindows\") || \"0\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayEvolutionV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n collapseWindows,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/metrics\") {\n res.writeHead(200, { \"Content-Type\": \"text/plain; version=0.0.4; charset=utf-8\" });\n res.end(renderInfraMetricsProm());\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.infraPrometheusMetrics) {\n if (process.env.CASTLE_PROMETHEUS_METRICS !== \"1\") {\n sendJson(res, 404, { ok: false, error: \"prometheus_metrics_disabled\" });\n return;\n }\n res.writeHead(200, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(renderRhizohPrometheusMetrics());\n return;\n }\n if (req.method === \"GET\" && pathname.startsWith(\"/infra/traces/\")) {\n const traceId = decodeURIComponent(pathname.slice(\"/infra/traces/\".length));\n const item = await getTraceById(traceId);\n if (!item) return sendJson(res, 404, { ok: false, error: \"trace_not_found\" });\n return sendJson(res, 200, { ok: true, trace: item });\n }\n if (req.method === \"GET\" && pathname === \"/infra/traces\") {\n const u = new URL(String(req.url || \"/infra/traces\"), `http://${req.headers.host || \"localhost\"}`);\n const sessionId = String(u.searchParams.get(\"sessionId\") || \"\").trim();\n const items = sessionId ? await listTracesBySession(sessionId) : [];\n return sendJson(res, 200, { ok: true, sessionId, traces: items });\n }\n\n /** Presence room mesh (C): JOIN / LEAVE / SNAPSHOT / DELTA / REPLAY (HTTP); SUBSCRIBE = SSE stream. */\n if (req.method === \"POST\" && pathname === \"/presence/mesh/join\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid, clientUid: actor.clientUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/leave\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshLeave(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/snapshot\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n sendJson(res, 200, meshSnapshot(roomUid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/delta\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, MAX_MESSAGE_BYTES);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const seq = meshAppendDelta(roomUid, actor.clientUid, {\n node: body?.node,\n projectionPatch: body?.projectionPatch,\n writerSubject: body?.writerSubject\n });\n sendJson(res, 200, { ok: true, roomUid, seq });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/replay\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const fromNodeId = body?.fromNodeId != null ? String(body.fromNodeId) : \"\";\n const fromSeq = body?.fromSeq;\n if (fromNodeId) {\n sendJson(res, 200, meshReplayFromNodeId(roomUid, fromNodeId));\n return;\n }\n sendJson(res, 200, meshReplayFromSeq(roomUid, fromSeq));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/presence/mesh/subscribe\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const roomUid = String(q.searchParams.get(\"roomUid\") || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n const write = (chunk) => {\n try {\n res.write(chunk);\n } catch {\n /* closed */\n }\n };\n const unsub = meshSubscribeSse(roomUid, write);\n const onClose = () => {\n unsub();\n meshLeave(roomUid, actor.clientUid);\n try {\n res.end();\n } catch {\n /* ignore */\n }\n };\n req.on(\"close\", onClose);\n req.on(\"aborted\", onClose);\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, uid: auth.uid, mode: auth.mode, items: await listConnections(auth.uid) });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const id = await createConnection(auth.uid, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"create_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n await updateConnection(auth.uid, id, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"update_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"DELETE\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n await deleteConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"delete_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections/test\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n if (!checkHttpRateLimit(`llm_conn_test:${auth.uid}`, RL_LLM_CONN_TEST_PER_MIN, 60_000)) {\n return sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n }\n const hasBodyKey = !!String(payload?.apiKey || \"\").trim();\n const result = await queryRhizohLlm(\n {\n message: String(payload?.message || \"Ping from Castle Gateway\"),\n context: payload?.context || { source: \"connection_test\" },\n provider: payload?.provider,\n model: payload?.model,\n apiKey: payload?.apiKey || \"\"\n },\n { allowExternalApiKey: true, llmKeySource: hasBodyKey ? \"auto\" : \"env\" }\n );\n logLlmAccess({\n route: \"/llm/connections/test\",\n uid: auth.uid,\n llmKeyBillingOwner: result.llmKeyBillingOwner,\n llmKeyOrigin: result.llmKeyOrigin,\n provider: result.provider,\n model: result.model,\n connectionId: null\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"connection_test_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAgentIdentities(auth.uid);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerAgentIdentity({\n ownerUid: auth.uid,\n agentId: payload?.agentId,\n role: payload?.role,\n personaSeed: payload?.personaSeed || {},\n capabilityLevel: payload?.capabilityLevel\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/agents/identities/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const owns = await getAgentIdentity(id);\n if (!owns || owns.ownerUid !== auth.uid) return sendJson(res, 403, { ok: false, error: \"forbidden\" });\n const payload = await readHttpJson(req);\n const row = await updateAgentIdentity(id, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/academy/events\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/academy/events\"}`);\n const status = String(url.searchParams.get(\"status\") || \"\");\n const items = await listAcademyEvents({ ownerUid: auth.uid, status, limit: 120 });\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/academy/events\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const ev = await queueAcademyEvent({\n type: payload?.type,\n roomId: payload?.roomId,\n ownerUid: auth.uid,\n participants: payload?.participants || [],\n topic: payload?.topic || \"academy\",\n payload: payload?.payload || {}\n });\n sendJson(res, 200, { ok: true, event: ev });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && String(req.url || \"\").startsWith(\"/academy/events/\") && String(req.url || \"\").endsWith(\"/resolve\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const parts = String(req.url || \"\").split(\"/\").filter(Boolean);\n const eventId = parts[2] || \"\";\n try {\n const payload = await readHttpJson(req);\n const out = await resolveAcademyEvent(eventId, payload || {});\n sendJson(res, 200, { ok: true, event: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_resolve_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/memory\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/memory\"}`);\n const scope = url.searchParams.get(\"scope\") === \"agents\" ? \"agents\" : \"users\";\n const id = String(url.searchParams.get(\"id\") || (scope === \"agents\" ? \"\" : auth.uid));\n const kind = String(url.searchParams.get(\"kind\") || \"\");\n const limit = Number(url.searchParams.get(\"limit\") || 40);\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n sendJson(res, 200, { ok: true, scope, id, kind, items: await listMemories({ scope, id, kind, limit }) });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const profile = await getPersonaGoalMemory(auth.uid);\n sendJson(res, 200, { ok: true, profile });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const profile = await setPersonaGoalMemory({\n uid: auth.uid,\n personaPatch: payload?.personaPatch || {},\n goals: Array.isArray(payload?.goals) ? payload.goals : null,\n preferencesPatch: payload?.preferencesPatch || {}\n });\n sendJson(res, 200, { ok: true, profile });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"profile_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/compact\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const out = await autoCompactMemories({ scope, id });\n sendJson(res, 200, { ok: true, scope, id, ...out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_compact_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/context\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const context = await getMemoryContext({\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n query: String(payload?.query || \"\"),\n limit: Number(payload?.limit || 80)\n });\n sendJson(res, 200, { ok: true, context });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_context_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/place-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const placeName = String(payload?.placeName || \"\").trim();\n if (!placeName) return sendJson(res, 400, { ok: false, error: \"placeName_required\" });\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const persona = String(payload?.waypointPersona || payload?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(placeName)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Place: ${placeName}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Give a concise conversational briefing about this place for a walking-route companion.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-place-brief\", placeName, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `PLACE_BRIEF:${placeName} => ${String(summary.reply || \"\").slice(0, 900)}`,\n tags: [\"place\", \"brief\", \"event-layer\"],\n importance: 0.62,\n meta: { placeName }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"place-brief\",\n text: `${placeName} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: {\n placeName,\n directive: summary.directive,\n lat: hit?.lat ? Number(hit.lat) : null,\n lon: hit?.lon ? Number(hit.lon) : null\n }\n });\n sendJson(res, 200, { ok: true, place: hit || null, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"place_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/route-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const waypoints = Array.isArray(payload?.waypoints) ? payload.waypoints.slice(0, 24) : [];\n const out = [];\n for (const wp of waypoints) {\n const name = String(wp?.name || \"\").trim();\n if (!name) continue;\n const persona = String(wp?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(name)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Waypoint: ${name}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Generate a concise spoken briefing for route waypoint.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-route-brief\", waypoint: name, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n out.push({\n name,\n persona,\n place: hit || null,\n reply: summary.reply,\n directive: summary.directive\n });\n }\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `ROUTE_BRIEF: waypoints=${out.length} ${out.map((x) => `${x.name}:${x.persona}`).join(\" | \")}`.slice(0, 1300),\n tags: [\"route\", \"waypoint\", \"brief\", \"event-layer\"],\n importance: 0.74,\n meta: { count: out.length }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"route-brief\",\n text: out.map((x) => `${x.name}:${String(x.reply || \"\").slice(0, 140)}`).join(\" | \").slice(0, 1400),\n roomId: \"studio-main\",\n meta: {\n waypointCount: out.length,\n waypoints: out.map((x) => ({\n name: x.name,\n persona: x.persona,\n lat: x.place?.lat ? Number(x.place.lat) : null,\n lon: x.place?.lon ? Number(x.place.lon) : null\n }))\n }\n });\n sendJson(res, 200, { ok: true, items: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"route_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/pdf-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const title = String(payload?.title || \"document\");\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const providedText = String(payload?.text || \"\");\n const url = String(payload?.url || \"\");\n let content = providedText;\n if (!content && url) content = await fetchTextFromUrl(url);\n if (!content) return sendJson(res, 400, { ok: false, error: \"text_or_supported_url_required\" });\n const summary = await queryRhizohLlm(\n {\n message: `Summarize this document and provide key points + practical actions.\\nTitle:${title}\\nContent:\\n${content.slice(0, 12000)}`,\n context: { source: \"event-layer-pdf-brief\", title },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `DOC_BRIEF:${title} => ${String(summary.reply || \"\").slice(0, 1000)}`,\n tags: [\"document\", \"brief\", \"library\", \"event-layer\"],\n importance: 0.7,\n meta: { title, url: url || null }\n });\n await addTranscript(auth.uid, {\n source: \"library-brief\",\n eventType: \"pdf-brief\",\n text: `${title} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { title }\n });\n sendJson(res, 200, { ok: true, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/pdf/upload\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req, 8 * 1024 * 1024);\n const job = await enqueuePdfJob({\n uid: auth.uid,\n fileName: payload?.fileName || \"document.pdf\",\n bytesBase64: payload?.contentBase64\n });\n sendJson(res, 200, { ok: true, job });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_upload_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/session\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createPublishSession(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session\",\n text: `Publish session armed: ${row.protocol} ${row.target}`.slice(0, 500),\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: row.id, bridge: row.bridge }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/studio/publish/sessions\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listPublishSessions(auth.uid, 80);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/health-check\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const protocol = String(payload?.protocol || \"WHIP\");\n const target = String(payload?.target || \"\");\n const health = await checkPublishTargetHealth(protocol, target);\n if (payload?.sessionId) {\n await updatePublishSession(auth.uid, String(payload.sessionId), {\n status: health.ok ? \"healthy\" : \"degraded\",\n reconnectPolicy: {\n enabled: true,\n backoffSec: [2, 5, 10, 20],\n nextRetryInSec: health.reconnectInSec || 0,\n maxRetries: 8,\n reason: health.reason\n }\n });\n }\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-health\",\n text: `${protocol} ${target} => ${health.ok ? \"healthy\" : \"degraded\"} (${health.reason})`,\n roomId: \"studio-main\",\n meta: { protocol, target, reconnect: !!health.shouldReconnect }\n });\n sendJson(res, 200, { ok: true, health });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_health_check_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/studio/publish/sessions/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updatePublishSession(auth.uid, id, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session-update\",\n text: `Session ${id} -> ${row.status || \"updated\"}`,\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: id, status: row.status, ingestUrl: row.ingestUrl || \"\" }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/transcripts\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/transcripts\"}`);\n const limit = Math.max(1, Math.min(200, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await listTranscripts(auth.uid, limit);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/transcripts\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await addTranscript(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"transcript_write_failed\" });\n }\n return;\n }\n\n /** E1: broadcast intent → GreenRoom capability → transcript + memory artifact */\n if (req.method === \"POST\" && req.url === \"/studio/capabilities/greenroom\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const traceId = String(payload?.traceId || `TRC-${Date.now().toString(36).toUpperCase()}`);\n const title = String(payload?.title || \"Castle Live Broadcast\").slice(0, 200);\n const intentRaw = String(payload?.intentRaw || \"\").slice(0, 800);\n const audienceEstimate = Math.max(0, Math.min(50_000_000, Number(payload?.audienceEstimate) || 1200));\n const roomId = String(payload?.roomId || \"greenroom-main\");\n const transcriptRow = await addTranscript(auth.uid, {\n source: \"rhizoh\",\n eventType: \"BROADCAST_ROUTED\",\n text: `GreenRoom · ${title}${intentRaw ? `\\nIntent: ${intentRaw}` : \"\"}`,\n roomId,\n meta: {\n ack: \"BROADCAST_ROUTED\",\n capability: \"greenroom\",\n intentType: \"broadcast\",\n traceId,\n audienceEstimate,\n artifact: {\n kind: \"broadcast_card\",\n title,\n traceId,\n roomId,\n createdAt: Date.now()\n }\n }\n });\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n text: `GREENROOM_BROADCAST:${title} trace=${traceId}`,\n tags: [\"broadcast\", \"greenroom\", \"artifact\", \"castle-library\"],\n importance: 0.72,\n kind: \"episodic\",\n meta: { traceId, capability: \"greenroom\", transcriptId: transcriptRow.id }\n });\n sendJson(res, 200, {\n ok: true,\n ack: \"BROADCAST_ROUTED\",\n traceId,\n transcript: transcriptRow,\n replayPath: `/replay/${encodeURIComponent(traceId)}`,\n sharePath: `/replay/${encodeURIComponent(traceId)}`\n });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"greenroom_capability_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/metadata/search\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/metadata/search\"}`);\n const q = String(url.searchParams.get(\"q\") || \"\");\n const limit = Math.max(1, Math.min(300, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await searchMetadata(auth.uid, q, limit);\n sendJson(res, 200, { ok: true, q, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerDevice(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, device: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"device_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const lat = Number(payload?.lat);\n const lon = Number(payload?.lon);\n const speed = Math.max(0, Number(payload?.speed || 0));\n const cfgCenterLat = Number(process.env.CASTLE_GEOFENCE_CENTER_LAT || 41.0082);\n const cfgCenterLon = Number(process.env.CASTLE_GEOFENCE_CENTER_LON || 28.9784);\n const cfgRadiusM = Math.max(100, Number(process.env.CASTLE_GEOFENCE_RADIUS_M || 55000));\n const cfgSpeedCap = Math.max(1, Number(process.env.CASTLE_SPEED_CAP_MPS || 35));\n const dist = Number.isFinite(lat) && Number.isFinite(lon) ? haversineMeters(cfgCenterLat, cfgCenterLon, lat, lon) : 0;\n const geofenceOk = !Number.isFinite(dist) ? true : dist <= cfgRadiusM;\n const speedOk = speed <= cfgSpeedCap;\n const failsafe = !(geofenceOk && speedOk);\n const safety = {\n geofenceOk,\n speedOk,\n failsafe,\n distanceFromCenterM: Number.isFinite(dist) ? dist : null,\n speedCapMps: cfgSpeedCap\n };\n const row = await appendTelemetry(auth.uid, deviceId, {\n lat,\n lon,\n speed,\n battery: payload?.battery,\n mode: payload?.mode || (failsafe ? \"FAILSAFE_HOLD\" : \"ACTIVE\"),\n raw: payload?.raw || {}\n });\n await updateDevice(auth.uid, deviceId, {\n status: failsafe ? \"failsafe\" : \"active\",\n lastTelemetryAt: row.ts,\n safetyState: safety\n });\n if (failsafe) {\n await addTranscript(auth.uid, {\n source: \"robotics-guardrails\",\n eventType: \"failsafe\",\n text: `Device ${deviceId} failsafe triggered geofenceOk=${geofenceOk} speedOk=${speedOk}`,\n roomId: \"studio-main\",\n meta: { deviceId, safety }\n });\n }\n sendJson(res, 200, { ok: true, telemetry: row, safety, command: failsafe ? \"FAILSAFE_HOLD\" : \"CONTINUE\" });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"telemetry_ingest_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listTelemetry(auth.uid, 150);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createAutomation(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"automation-scheduled\",\n text: `Automation scheduled: ${row.title} at ${new Date(row.scheduleAt).toLocaleString()}`,\n roomId: \"studio-main\",\n meta: { automationId: row.id, action: row.action, deviceId: row.deviceId }\n });\n sendJson(res, 200, { ok: true, automation: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"automation_create_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAutomations(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listDevices(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const devs = await listDevices(auth.uid, 500);\n const d = devs.find((x) => x.id === deviceId);\n if (!d) return sendJson(res, 404, { ok: false, error: \"device_not_found\" });\n const action = String(payload?.action || \"\");\n const caps = Array.isArray(d.capabilityProfile) ? d.capabilityProfile : [];\n if (action && caps.length && !caps.includes(action) && !caps.includes(\"*\")) {\n return sendJson(res, 400, { ok: false, error: \"unsupported_action_for_device\", capabilityProfile: caps });\n }\n const cmd = await enqueueCommand(auth.uid, {\n deviceId,\n action: action || \"NOOP\",\n params: payload?.params || {},\n adapter: payload?.adapter || d.adapter || \"websocket\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-command-queue\",\n eventType: \"command-queued\",\n text: `${cmd.deviceId} -> ${cmd.action}`,\n roomId: \"studio-main\",\n meta: { commandId: cmd.id, adapter: cmd.adapter }\n });\n sendJson(res, 200, { ok: true, command: cmd });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listCommands(auth.uid, 200);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/robotics/commands/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updateCommandStatus(auth.uid, id, payload?.status || \"sent\");\n sendJson(res, 200, { ok: true, command: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/rhizoh/bridge\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const result = await runRhizohBrainV2({\n message: String(payload?.message || \"robotics bridge command\"),\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n worldState: payload?.worldState || {},\n deviceState: payload?.deviceState || {},\n intent: \"robotics-mechanics\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-bridge\",\n eventType: \"robotics-command\",\n text: `Bridge -> ${String(result.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { directive: result.directive, agentId: payload?.agentId || \"\" }\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"robotics_bridge_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/planner/graph\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const graph = buildTaskActionGraph(String(payload?.task || \"\"), payload?.context || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"planner-graph\",\n text: `Planner graph built: ${graph.taskTitle} nodes=${graph.nodes.length}`,\n roomId: \"studio-main\",\n meta: { taskTitle: graph.taskTitle, nodeCount: graph.nodes.length }\n });\n sendJson(res, 200, { ok: true, graph });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"planner_graph_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerSocialChannel(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, channel: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_channel_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listSocialChannels(auth.uid, 100);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/broadcast\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const text = String(payload?.text || \"\").slice(0, 1200);\n const channels = await listSocialChannels(auth.uid, 100);\n const enabled = channels.filter((c) => c.status === \"enabled\");\n const deliveries = [];\n for (const c of enabled) {\n const out = await deliverSocialWithRetry(c, text);\n deliveries.push({ id: c.id, platform: c.platform, ...out });\n }\n const okCount = deliveries.filter((d) => d.ok).length;\n await addTranscript(auth.uid, {\n source: \"social-layer\",\n eventType: \"social-broadcast\",\n text: `Broadcast delivered ${okCount}/${enabled.length} channel(s): ${text.slice(0, 240)}`,\n roomId: \"studio-main\",\n meta: { deliveries }\n });\n sendJson(res, 200, { ok: true, sentChannels: okCount, totalChannels: enabled.length, deliveries });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_broadcast_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/ethics/programs\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, ...buildEthicsPrograms() });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands/dispatch\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const all = await listCommands(auth.uid, 200);\n const queued = all.filter((c) => String(c.status || \"queued\") === \"queued\").slice(0, 50);\n const devices = await listDevices(auth.uid, 500);\n const byId = new Map(devices.map((d) => [d.id, d]));\n const results = [];\n for (const cmd of queued) {\n const dev = byId.get(cmd.deviceId);\n if (!dev) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: \"device_not_found\" });\n continue;\n }\n try {\n const dispatched = await dispatchCommandToAdapter(dev, cmd);\n await updateCommandStatus(auth.uid, cmd.id, \"sent\");\n results.push({ id: cmd.id, ok: true, adapter: dispatched.adapter, via: dispatched.via });\n } catch (error) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: String(error?.message || \"dispatch_failed\") });\n }\n }\n await addTranscript(auth.uid, {\n source: \"robotics-command-dispatch\",\n eventType: \"command-dispatch-batch\",\n text: `Dispatch batch size=${results.length} ok=${results.filter((x) => x.ok).length}`,\n roomId: \"studio-main\",\n meta: { results: results.slice(0, 30) }\n });\n sendJson(res, 200, { ok: true, processed: results.length, results });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"dispatch_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/pdf/jobs/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n const job = await getPdfJob(auth.uid, id);\n if (!job) return sendJson(res, 404, { ok: false, error: \"pdf_job_not_found\" });\n sendJson(res, 200, { ok: true, job });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const row = await appendMemory({\n scope,\n id,\n text: payload?.text,\n tags: payload?.tags || [],\n importance: payload?.importance,\n kind: payload?.kind || \"episodic\",\n meta: { uid: auth.uid, ...(payload?.meta || {}) }\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.externalTruth) {\n try {\n if (process.env.RHIZOH_EXTERNAL_TRUTH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_truth:${ip}`, RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n sendJson(res, 200, buildRhizohExternalGroundTruthPayload(req));\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.productOutcome) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const rawBody = await readHttpBodyText(req, 64 * 1024);\n const body = rawBody.trim() ? safeJsonParse(rawBody) : null;\n if (!body || typeof body !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"invalid_json_payload\" });\n return;\n }\n const sigHdr = String(req.headers?.[\"x-rhizoh-outcome-signature\"] || \"\").trim();\n const hmacV = verifyRhizohOutcomeHmac(rawBody, sigHdr);\n if (!hmacV.ok) {\n sendJson(res, 401, { ok: false, error: hmacV.error || \"outcome_hmac_invalid\" });\n return;\n }\n const srcTokHdr = String(req.headers?.[\"x-rhizoh-outcome-source-token\"] || \"\").trim();\n const srcV = verifyRhizohOutcomeSourceToken(body.source, srcTokHdr);\n if (!srcV.ok) {\n sendJson(res, 401, { ok: false, error: srcV.error || \"outcome_source_token_invalid\" });\n return;\n }\n const subjectKey = String(body.subjectKey || \"\").trim().slice(0, 128);\n if (\n subjectKey &&\n !checkHttpRateLimit(\n `rhizoh_product_outcome_subj:${subjectKey}`,\n RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN,\n 60_000\n )\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_subject\" });\n return;\n }\n const result = ingestRhizohProductOutcomeHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n schemaVersion: \"1.1.0\",\n aggregate: result.aggregate,\n integrity: result.integrity\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"outcome_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.productOutcomeAggregate) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome_agg:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const u = new URL(String(req.url || \"/\"), \"http://localhost\");\n const cohortId = u.searchParams.get(\"cohortId\") || \"\";\n const decisionFingerprint = u.searchParams.get(\"decisionFingerprint\") || \"\";\n const limit = u.searchParams.get(\"limit\") || \"50\";\n const rows = listRhizohProductOutcomeAggregates({\n cohortId: cohortId || undefined,\n decisionFingerprint: decisionFingerprint || undefined,\n limit: Number(limit) || 50\n });\n sendJson(res, 200, { ok: true, schemaVersion: \"1.1.0\", rows });\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.externalLossBatch) {\n try {\n if (process.env.RHIZOH_EXTERNAL_LOSS_BATCH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_loss_batch:${ip}`, RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const result = ingestRhizohExternalLossBatchHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, { ok: true, accepted: result.accepted ?? 0 });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"batch_failed\") });\n }\n return;\n }\n\n function moderationAdminOk(req) {\n const expected = String(process.env.CASTLE_MODERATION_ADMIN_KEY || \"\").trim();\n if (!expected) return false;\n const got = String(req.headers[\"x-castle-moderation-key\"] || \"\").trim();\n return got.length > 0 && got === expected;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.opsAbuseReport) {\n try {\n const body = await readHttpJson(req, 32 * 1024);\n const auth = await resolveHttpUser(req);\n const entry = submitAbuseReportV0({\n reporterUid: auth.ok ? auth.uid : null,\n targetUid: body?.targetUid ?? null,\n category: body?.category,\n detail: body?.detail,\n traceId: body?.traceId ?? null\n });\n sendJson(res, 202, { ok: true, reportId: entry.id, status: entry.status });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsModerationQueue) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n queue: listModerationQueueV0(100),\n softBlocked: listSoftBlockedUsersV0()\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-block\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const out = softBlockUserV0(body?.uid, body?.reason);\n sendJson(res, out.ok ? 200 : 400, out);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-unblock\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n sendJson(res, 200, removeSoftBlockV0(body?.uid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsAgentSnapshots) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n const limit = Number(new URL(req.url || \"\", \"http://local\").searchParams.get(\"limit\") || 50);\n sendJson(res, 200, { ok: true, snapshots: listRecentAgentSnapshotsV0(limit) });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.academicObservatoryExport) {\n const auth = await resolveHttpUser(req);\n const out = handleAcademicObservatoryExportGetV0(req, auth);\n sendJson(res, out.status, out.body);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsHardeningStatus) {\n const auth = await resolveHttpUser(req);\n const principal = auth.ok ? `uid:${auth.uid}` : null;\n const gclSnap = await getGlobalCostLedgerSnapshotV0(principal);\n const economicStrategy = evaluateEconomicStrategyL3V0(gclSnap);\n const hardeningTenantId = resolveHardeningTenantIdV0(auth, req);\n const unifiedState = await buildUnifiedStateNarrativeV0({\n tenantId: hardeningTenantId ?? undefined,\n platformScope: !hardeningTenantId,\n principal\n });\n sendJson(res, 200, {\n ok: true,\n schema: \"rhizoh.ops.hardening_status.v0\",\n containment: readAgentContainmentConfigV0(),\n cost: readCostContainmentConfigV0(),\n gcl: {\n backend: await getGclBackendStatusV0(),\n snapshot: gclSnap\n },\n economicStrategyL3: economicStrategy,\n phasedRollout: await getPhasedRolloutStatsV0(),\n tier: readPhasedRolloutTierV0(),\n principalCost: auth.ok ? await getCostContainmentStatsV0(principal) : null,\n stressTaxonomy: verifyStressTaxonomyCoverageV0(),\n stressConflictResolution: verifyStressConflictResolutionV0(),\n resolutionStabilityEnvelope: verifyResolutionStabilityEnvelopeV0(),\n unifiedState: {\n tenantScope: {\n tenantId: unifiedState.tenantScope?.tenantId,\n scope: unifiedState.tenantScope?.scope,\n noGlobalDerivedState: unifiedState.tenantScope?.noGlobalDerivedState\n },\n narrativeFingerprint: unifiedState.narrativeFingerprint,\n screenshotScopeWatermark: unifiedState.screenshotScopeWatermark,\n interpretationSafetyContract: unifiedState.interpretationSafetyContract,\n interpretationUxContract: {\n schema: unifiedState.interpretationUxContract?.schema,\n layerOrder: unifiedState.interpretationUxContract?.layerOrder,\n narrativeNeverVisuallyDominant: unifiedState.interpretationUxContract?.narrativeNeverVisuallyDominant\n },\n governance: {\n narrativeIsNotDecisionLayer: unifiedState.governance.narrativeIsNotDecisionLayer,\n displayOrder: unifiedState.governance.displayOrder,\n trustPosture: unifiedState.governance.trustPosture,\n interpretationUxContract: unifiedState.governance.interpretationUxContract?.schema\n },\n humanOps: {\n decisionTier: unifiedState.humanOps?.humanDecisionScaling?.tier,\n decisionOwner: unifiedState.humanOps?.humanDecisionScaling?.decisionOwner,\n opsRouteId: unifiedState.humanOps?.humanDecisionOpsRunbook?.routing?.routeId,\n slaAckMinutes: unifiedState.humanOps?.humanDecisionOpsRunbook?.sla?.ackMinutes,\n uiDisclaimerRequired: unifiedState.humanOps?.narrativeUiSafety?.displayRules?.requireDisclaimerBanner,\n misreadHighResidual: unifiedState.humanOps?.misreadSimulation?.highResidualCount,\n propagationHighResidual: unifiedState.humanOps?.socialPropagationSimulation?.highResidualCount,\n worstWatermarkSurvivability:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.worstWatermarkSurvivability,\n dominantDistortionSource:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.dominantDistortionSource\n },\n culturalRisk: {\n perceptualRiskClass: unifiedState.culturalRisk?.perceptualRiskClass,\n trustFork: unifiedState.culturalRisk?.trustDynamics?.fork,\n trustDecayScore: unifiedState.culturalRisk?.trustDynamics?.scores?.trustDecay,\n mythologyScore: unifiedState.culturalRisk?.trustDynamics?.scores?.mythology,\n confidenceDetachmentExposure:\n unifiedState.culturalRisk?.trustDynamics?.axes?.confidenceDetachmentExposure\n },\n appliedSystemsLayer: {\n schema: unifiedState.appliedSystemsLayer?.schema,\n researchOutputType: unifiedState.appliedSystemsLayer?.researchGovernance?.outputType,\n roboticsBlockActuation: unifiedState.appliedSystemsLayer?.roboticsGrounding?.blockActuation,\n spiralRumorCount:\n unifiedState.appliedSystemsLayer?.spiralMMOGameKernel?.gameLayer?.rumors?.length\n },\n actionSemanticGovernance: {\n schema: unifiedState.actionSemanticGovernance?.schema,\n highRiskActionCount: unifiedState.actionSemanticGovernance?.highRiskActionCount,\n loopClosed: unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.loopClosed,\n authorityCollapseMitigated:\n unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.risks?.authorityCollapse\n ?.mitigated\n },\n actionContextResolution: {\n schema: unifiedState.actionContextResolution?.schema,\n contextFingerprint: unifiedState.actionContextResolution?.contextFingerprint?.fingerprintHash,\n domainCollision: unifiedState.actionContextResolution?.domainCollision?.detected,\n deployAgentSpiralBlocked:\n unifiedState.actionContextResolution?.actionOverloadingRisk?.deployAgentExample\n ?.spiral_mmo === \"blocked\",\n roboticsActuationEligible:\n unifiedState.actionContextResolution?.roboticsActuationCorrectness?.actuationEligible\n },\n epistemicCoherence: {\n schema: unifiedState.epistemicCoherence?.schema,\n verdict: unifiedState.epistemicCoherence?.systemCoherence?.verdict,\n coherenceScore: unifiedState.epistemicCoherence?.systemCoherence?.score,\n contradictionCount: unifiedState.epistemicCoherence?.crossLayerContradictions?.count,\n operatorDecision: unifiedState.epistemicCoherence?.uxCompression?.operatorDecision,\n headlineTr: unifiedState.epistemicCoherence?.uxCompression?.headline?.tr,\n falseSafetyWarning: Boolean(\n unifiedState.epistemicCoherence?.uxCompression?.falseSafetyWarning\n )\n },\n coherenceAuthorityBoundary: {\n schema: unifiedState.coherenceAuthorityBoundary?.schema,\n bindingValid: unifiedState.coherenceAuthorityBoundary?.eclAuthorityBinding?.valid,\n canExecute: unifiedState.coherenceAuthorityBoundary?.contract?.can_execute,\n mandatoryBannerTr:\n unifiedState.coherenceAuthorityBoundary?.disclaimers?.mandatoryBanner?.tr\n },\n decisionLatencyGovernance: {\n schema: unifiedState.decisionLatencyGovernance?.schema,\n latencyTier: unifiedState.decisionLatencyGovernance?.latencyTier,\n inflationScore: unifiedState.decisionLatencyGovernance?.latencyInflation?.score,\n fastPathEligible: unifiedState.decisionLatencyGovernance?.fastPath?.eligible,\n primaryActionId: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.primaryActionId,\n ackSlaMinutes: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.ackSlaMinutes,\n certaintyCap: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.certaintyCap,\n uncertaintyItems:\n unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.uncertaintyEnvelope?.itemCount\n },\n decisionPacketUncertaintyBoundary: {\n schema: unifiedState.decisionPacketUncertaintyBoundary?.schema,\n invariantValid: unifiedState.decisionPacketUncertaintyBoundary?.invariantCheck?.valid,\n centralInvariant: unifiedState.decisionPacketUncertaintyBoundary?.centralInvariant\n },\n epistemicDecisionPacing: {\n schema: unifiedState.epistemicDecisionPacing?.schema,\n queueSaturationTier:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation?.tier,\n operatorProcessingLatency:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation\n ?.operatorProcessingLatency,\n postureWindowCount:\n unifiedState.epistemicDecisionPacing?.temporaryStaticPostureWindows\n ?.temporaryStaticPostureWindows?.windowCount,\n executionBoundariesValid:\n unifiedState.epistemicDecisionPacing?.executionBoundaries?.valid,\n grantsExecutionApproval: unifiedState.epistemicDecisionPacing?.grantsExecutionApproval\n },\n epistemicTemporalCoherence: {\n schema: unifiedState.epistemicTemporalCoherence?.schema,\n allInvariantsValid: unifiedState.epistemicTemporalCoherence?.allInvariantsValid,\n temporalAlignmentValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.temporalAlignment?.valid,\n crossWindowGuardValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard?.valid,\n delayedTruthValid: unifiedState.epistemicTemporalCoherence?.invariants?.delayedTruthRule?.valid,\n reconciliationRequired:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard\n ?.reconciliation?.required\n },\n realityDriftObserver: {\n schema: unifiedState.realityDriftObserver?.schema,\n driftActive: unifiedState.realityDriftObserver?.driftObserverSummary?.active,\n misapprehensionShapeCount:\n unifiedState.realityDriftObserver?.misapprehensionShapeCatalog?.shapeCount,\n criticalShapeCount:\n unifiedState.realityDriftObserver?.driftObserverSummary?.criticalShapeCount,\n roboticsMismatchActive:\n unifiedState.realityDriftObserver?.roboticsFeedbackMismatch?.active\n },\n driftCausality: {\n schema: unifiedState.driftCausality?.schema,\n shapesWithCausalExplanation: unifiedState.driftCausality?.rdolLinkage?.shapesWithCausalExplanation,\n activeDomainCount: unifiedState.driftCausality?.causalitySummary?.activeDomainCount,\n propagationCausalityActive: unifiedState.driftCausality?.causalDomains?.propagation?.active\n },\n productReadiness: unifiedState.productReadiness,\n stateLayers: {\n raw: unifiedState.stateLayers.raw,\n derived: {\n systemState: unifiedState.stateLayers.derived.systemState,\n narrative: unifiedState.stateLayers.derived.narrative,\n validation: unifiedState.stateLayers.derived.validation\n },\n policy: {\n binding: unifiedState.stateLayers.policy.binding,\n decisionOwner: unifiedState.stateLayers.policy.decisionOwner,\n prohibitedActions: unifiedState.stateLayers.policy.prohibitedActions\n }\n },\n systemState: unifiedState.systemState,\n interpretation: unifiedState.interpretation,\n validation: {\n trustPosture: unifiedState.validation.trustPosture,\n adjustedConfidence: unifiedState.validation.adjustedConfidence,\n agreement: unifiedState.validation.confidenceDecomposition.agreement,\n divergenceScore: unifiedState.validation.divergence.divergenceScore,\n validated: unifiedState.validation.divergence.validated,\n uncertaintyTags: unifiedState.validation.uncertainty.tags.map((t) => t.id),\n flags: unifiedState.validation.divergence.flags.map((f) => f.id)\n },\n compression: unifiedState.compression\n }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 6 * 1024 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (\n !checkHttpRateLimit(`rhizoh_voice_transcribe:${rlKey}`, RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN, 60_000)\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const voiceEnv = rhizohVoiceTranscribeEnvV3();\n if (!voiceEnv.any) {\n sendJson(res, 503, {\n ok: false,\n error: \"voice_asr_not_configured\",\n env: voiceEnv\n });\n return;\n }\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n const result = await runRhizohVoiceTranscribeV3(\n applyLanguagePropagationToVoiceBodyV1(payload, langProp)\n );\n sendJson(res, result.ok ? 200 : 422, {\n ...result,\n traceId: String(payload?.traceId || \"\"),\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n atMs: Date.now()\n });\n } catch (error) {\n sendJson(res, 500, {\n ok: false,\n error: \"voice_transcribe_failed\",\n detail: String(error?.message || error),\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(\"/rhizoh/llm/task/\")) {\n try {\n const rawTaskId = decodeURIComponent(pathname.slice(\"/rhizoh/llm/task/\".length).split(\"/\")[0] || \"\");\n const taskId = String(rawTaskId || \"\").toLowerCase() === \"last\" ? getLatestLlmWorkerTaskIdV0() : rawTaskId;\n const snap = taskId ? getLlmWorkerTaskSnapshotV0(taskId) : null;\n if (!snap) {\n sendJson(res, 404, {\n ok: false,\n error: \"llm_task_not_found\",\n taskId: rawTaskId,\n status: \"missing\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n if (snap.status === \"processing\") {\n sendJson(res, 202, {\n ok: true,\n taskId: snap.taskId,\n status: \"processing\",\n createdAt: snap.createdAt,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n // Terminal states (completed/failed) always HTTP 200 — poll must not look like server fault (500).\n sendJson(res, 200, {\n ...(snap.body || { ok: false, error: \"llm_task_empty\", taskId: snap.taskId }),\n taskId: snap.taskId,\n status: snap.status === \"completed\" ? \"completed\" : snap.status,\n terminal: true,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n } catch (error) {\n console.error(\"[llm-task-poll] handler failed:\", error);\n sendJson(res, 200, {\n ok: false,\n error: \"llm_task_poll_handler_failed\",\n detail: String(error?.message || error),\n status: \"failed\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === rhizohRuntime.routes.rhizohLlm) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 128 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n logRhizohHealth(\"gateway_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: String(payload?.traceId || \"\"),\n auth: auth.ok ? \"ok\" : \"anon\",\n ip\n });\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (!checkHttpRateLimit(`rhizoh_llm:${rlKey}`, RL_RHIZOH_LLM_PER_MIN, 60_000)) {\n const rlTaxonomy = classifyStressResponseV0({ code: \"rate_limit_exceeded\" });\n return sendJson(res, 429, {\n ok: false,\n error: \"rate_limit_exceeded\",\n reply: rlTaxonomy.userMessageTr || \"İstek sınırı aşıldı. Kısa süre sonra tekrar deneyin.\",\n stressClass: rlTaxonomy.stressClass,\n responseAction: rlTaxonomy.responseAction,\n stressMatrix: rlTaxonomy.matrix,\n stressInterpretable: rlTaxonomy.interpretable,\n stressConfidence: rlTaxonomy.stressConfidence,\n actionConfidence: rlTaxonomy.actionConfidence,\n actionSoftened: rlTaxonomy.actionSoftened,\n responseActionStrict: rlTaxonomy.responseActionStrict,\n actionInterpretable: rlTaxonomy.actionInterpretable,\n conflictResolution: rlTaxonomy.conflictResolution,\n directive: \"NONE\"\n });\n }\n\n const requireExplicit = process.env.CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE === \"1\";\n const keyMode = normalizeClientLlmKeySource(payload?.llmKeySource ?? payload?.keySource);\n if (keyMode === null) {\n return sendJson(res, 400, {\n ok: false,\n error: \"invalid_llm_key_source\",\n reply: \"llmKeySource geçersiz. Kullanın: env | user_connection | auto\",\n directive: \"NONE\"\n });\n }\n if (requireExplicit && keyMode === \"auto\") {\n return sendJson(res, 400, {\n ok: false,\n error: \"llm_key_source_required\",\n reply: \"Sunucu CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE=1: istekte llmKeySource (env veya user_connection) zorunlu.\",\n directive: \"NONE\"\n });\n }\n\n const conn = auth.ok ? await resolveConnection(auth.uid, String(payload?.connectionId || \"\")) : null;\n\n if (keyMode === \"user_connection\") {\n if (!auth.ok) {\n return sendJson(res, 401, {\n ok: false,\n error: \"llm_auth_required_for_user_connection\",\n reply: \"Kayıtlı LLM bağlantısı için giriş gerekli.\",\n directive: \"NONE\"\n });\n }\n if (!conn?.apiKey) {\n return sendJson(res, 400, {\n ok: false,\n error: \"user_llm_connection_required\",\n reply: \"Geçerli bir LLM bağlantısı yok; Studio’da bağlantı ekleyin veya llmKeySource=env kullanın.\",\n directive: \"NONE\"\n });\n }\n }\n\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n let safePayload = applyLanguagePropagationToLlmPayloadV1({ ...(payload || {}) }, langProp);\n const trimOut = trimRhizohLlmGatewayPayloadV1(safePayload);\n if (trimOut.meta.trimmed) {\n logRhizohHealth(\"llm_payload_trimmed\", {\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n bytesBefore: trimOut.meta.bytesBefore,\n bytesAfter: trimOut.meta.bytesAfter\n });\n }\n safePayload = trimOut.payload;\n delete safePayload.apiKey;\n delete safePayload.llmKeySource;\n delete safePayload.keySource;\n\n const llmMessage = String(\n safePayload?.message || safePayload?.text || payload?.message || payload?.text || \"\"\n ).trim();\n if (!llmMessage) {\n return sendJson(res, 400, {\n ok: false,\n error: \"message_required\",\n reply: \"Mesaj metni gerekli (message alanı boş).\",\n directive: \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n if (!safePayload.message) {\n safePayload = { ...safePayload, message: llmMessage };\n }\n\n let resolvedProvider = payload?.provider;\n let resolvedModel = payload?.model;\n let connApiKey = \"\";\n\n if (keyMode === \"user_connection\") {\n resolvedProvider = conn.provider;\n resolvedModel = conn.model;\n connApiKey = conn.apiKey || \"\";\n } else if (keyMode === \"env\") {\n connApiKey = \"\";\n } else {\n resolvedProvider = conn?.provider || payload?.provider;\n resolvedModel = conn?.model || payload?.model;\n connApiKey = conn?.apiKey || \"\";\n }\n\n const turnInput = {\n safePayload,\n auth,\n keyMode,\n conn,\n resolvedProvider,\n resolvedModel,\n connApiKey\n };\n const turnMeta = {\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n langEcho: buildRhizohLanguagePropagationEchoV1(langProp)\n };\n\n if (isLlmWorkerEnabledV0() && isLlmWorkerAsyncEnabledV0()) {\n const taskId = enqueueLlmWorkerTaskAsyncV0(turnInput, turnMeta);\n logRhizohHealth(\"llm_worker_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n taskId,\n traceId: String(payload?.traceId || taskId),\n keyMode,\n async: true\n });\n sendJson(res, 202, {\n ok: true,\n taskId,\n status: \"processing\",\n traceId: String(payload?.traceId || taskId),\n pollPath: `/rhizoh/llm/task/${taskId}`,\n gatewayPid: process.pid,\n ...turnMeta.langEcho\n });\n return;\n }\n\n const { result, traceId, turnLatencyMs, spinePhases, sampledTrace } = isLlmWorkerEnabledV0()\n ? await runLlmWorkerTaskV0(turnInput, turnMeta)\n : await rhizohGatewayTurn(turnInput);\n logRhizohHealth(\"llm_response\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId,\n turnLatencyMs: Number(turnLatencyMs || 0),\n keyMode\n });\n sendJson(res, 200, {\n ...result,\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n traceId,\n turnLatencyMs,\n ...(spinePhases ? { spinePhases } : {}),\n ...(sampledTrace !== undefined ? { sampledTrace } : {})\n });\n } catch (error) {\n const msg = String(error?.message || \"\");\n const code = error?.code || \"\";\n let status = 500;\n if (msg === \"payload_too_large\") status = 413;\n else if (msg === \"rate_limit_exceeded\") status = 429;\n else if (\n code === \"cost_hard_limit\" ||\n code === \"phased_rollout_capacity\" ||\n error?.containment === true ||\n String(code).startsWith(\"agent_\")\n ) {\n status = 429;\n } else if (code === \"user_soft_blocked\" || code === \"prompt_abuse_detected\") status = 403;\n else if (\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(code) ||\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(msg) ||\n msg.includes(\"missing_api_key_for_\")\n )\n status = 400;\n let rhizohFailureKind = \"provider_error\";\n if (msg === \"rate_limit_exceeded\" || status === 429) rhizohFailureKind = \"rate_limit\";\n else if (status === 403) rhizohFailureKind = \"policy_block\";\n else if (status === 400 || status === 413) rhizohFailureKind = \"client_config\";\n else if (msg.startsWith(\"provider_http_\")) {\n const st = Number(msg.replace(\"provider_http_\", \"\"));\n if (st === 408 || st === 504) rhizohFailureKind = \"timeout\";\n else rhizohFailureKind = \"provider_error\";\n } else if (/timeout|timed out|ETIMEDOUT/i.test(msg)) rhizohFailureKind = \"timeout\";\n\n const providerHttpMatch = msg.match(/^provider_http_(\\d{3})$/);\n const stressTaxonomy =\n error?.stressClass != null\n ? {\n stressClass: error.stressClass,\n responseAction: error.responseAction,\n stressMatrix: error.stressMatrix,\n stressInterpretable: error.stressInterpretable !== false,\n stressConfidence: error.stressConfidence,\n actionConfidence: error.actionConfidence,\n actionSoftened: error.actionSoftened,\n responseActionStrict: error.responseActionStrict,\n actionInterpretable: error.actionInterpretable,\n conflictResolution: error.conflictResolution,\n stressSecondary: error.stressSecondary,\n responseActions: error.responseActions\n }\n : (() => {\n const t = classifyStressResponseV0({\n code: code || msg,\n rhizohFailureKind,\n providerHttpStatus: providerHttpMatch\n ? Number(providerHttpMatch[1])\n : null\n });\n return {\n stressClass: t.stressClass,\n responseAction: t.responseAction,\n stressMatrix: t.matrix,\n stressInterpretable: t.interpretable,\n stressConfidence: t.stressConfidence,\n actionConfidence: t.actionConfidence,\n actionSoftened: t.actionSoftened,\n responseActionStrict: t.responseActionStrict,\n actionInterpretable: t.actionInterpretable,\n conflictResolution: t.conflictResolution,\n stressSecondary: t.stressSecondary,\n responseActions: t.responseActions\n };\n })();\n sendJson(res, status, {\n ok: false,\n rhizohFailureKind,\n ...(providerHttpMatch ? { providerHttpStatus: Number(providerHttpMatch[1]) } : {}),\n error: code || msg || \"rhizoh_llm_failed\",\n detail: msg || code || undefined,\n reply:\n error?.reply ||\n (status === 500 ? \"Rhizoh bağlantısı geçici olarak kesildi.\" : msg),\n directive: error?.directive || \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n ...stressTaxonomy\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicSeal) {\n try {\n if (!EPISTEMIC_SEAL_SECRET) {\n sendJson(res, 503, { ok: false, error: \"epistemic_seal_disabled\" });\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`epistemic_seal:${ip}`, RL_EPISTEMIC_SEAL_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const truth = body.truth_contract;\n if (!truth || typeof truth !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"truth_contract_required\" });\n return;\n }\n const sealBody = {\n truth_contract: truth,\n runtime_hash: String(body.runtime_hash || \"\"),\n model_route: {\n provider: body.model_route?.provider ?? null,\n model: body.model_route?.model ?? null\n },\n memory_digest: String(body.memory_digest || \"\"),\n world_snapshot_hash: String(body.world_snapshot_hash || \"\"),\n timestamp: Number(body.timestamp) || Date.now()\n };\n const canonical = canonicalEpistemicSealString(sealBody);\n const { hash, signature } = hashAndSignEpistemicSeal(canonical, EPISTEMIC_SEAL_SECRET);\n recordGenesisEpistemicSealIssued(hash);\n const attestation = {\n schema: EPISTEMIC_SEAL_SCHEMA,\n algorithm: \"SHA-256+HMAC-SHA256\",\n keyHint: process.env.CASTLE_EPISTEMIC_SEAL_SECRET ? \"CASTLE_EPISTEMIC_SEAL_SECRET\" : \"CASTLE_GATEWAY_TOKEN\",\n issuedAt: Date.now(),\n gateway: { service: \"castle-gateway\", listenPort: PORT }\n };\n sendJson(res, 200, {\n ok: true,\n hash,\n signature,\n attestation,\n canonicalBytes: Buffer.byteLength(canonical, \"utf8\")\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"seal_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicLogsBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`epistemic_logs:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 120) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const persisted = await persistEpistemicLedgerBatch(auth.uid, entries);\n recordGenesisEpistemicLedgerPersisted(persisted.persisted);\n // Internal cognition layer: forecast stays gateway/ledger-only (no client exposure).\n try {\n await persistEpistemicForecastBatch(auth.uid, persisted.normalized || []);\n } catch {\n /* forecast failures must not break primary ledger ingest */\n }\n sendJson(res, 200, {\n ok: true,\n persisted: persisted.persisted,\n mode: persisted.mode,\n latest: persisted.normalized?.[persisted.normalized.length - 1] || null\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"epistemic_log_ingest_failed\") });\n }\n return;\n }\n\n sendJson(res, 404, { ok: false, error: \"not_found\" });\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\nspiralWssForGenesis = wss;\nconst orchestrator = createOrchestrator();\nconst clientStats = new Map();\nlet broadcasterClientId = null;\nconst spiralState = {\n rooms: [{ id: \"room-default\", name: \"Spiral Default Arena\" }],\n activeByClient: new Map(),\n characters: new Map() // id -> {id,name,role,roomId,source}\n};\n\n/** Castle social rooms — keyed by `castleRoomKey` from {@link WS_MESSAGE.CASTLE_SOCIAL_PULSE}. */\nconst castleSocialRoomState = new Map();\nconst CASTLE_SOCIAL_MEMBER_TTL_MS = 90_000;\nconst CASTLE_SOCIAL_MAX_PULSE_BYTES = 8192;\nconst RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES = 5 * 1024 * 1024;\nconst RHIZOH_VOICE_LIVE_MAX_CHUNKS = 96;\n\n/**\n * Voice Engine v3 live media lane.\n * Client streams MediaRecorder chunks over the existing gateway WS; STOP reuses the\n * gateway ASR runner so orchestration remains the single authority downstream.\n */\nconst rhizohVoiceLiveSessions = new WeakMap();\n\nfunction appendRhizohVoiceLiveEventV0(session, type, payloadRefSeed = \"\") {\n if (!session) return null;\n session.eventSeq = (Number(session.eventSeq) || 0) + 1;\n const event = createRhizohImmutableEventV0({\n type,\n sessionId: session.sessionId,\n traceId: session.traceId,\n actorSeed: session.sessionId || session.traceId,\n eventSeq: session.eventSeq,\n payloadRef: createRhizohPayloadRefV0(payloadRefSeed || `${session.sessionId}:${type}:${session.eventSeq}`)\n });\n session.events = [...(session.events || []), event].slice(-64);\n return event;\n}\n\nfunction sendRhizohVoiceLiveEnvelopeV0(socket, type, payload) {\n try {\n if (socket?.readyState === 1) socket.send(JSON.stringify(createEnvelope(type, payload)));\n } catch {\n /* noop */\n }\n}\n\nfunction decodeRhizohVoiceLiveChunkV0(payload = {}) {\n const b64 = String(payload.audioBase64 || \"\").trim();\n if (!b64) return { ok: false, error: \"audio_base64_required\" };\n try {\n const buf = Buffer.from(b64, \"base64\");\n if (!buf.length) return { ok: false, error: \"audio_decode_empty\" };\n return { ok: true, buffer: buf };\n } catch {\n return { ok: false, error: \"audio_base64_invalid\" };\n }\n}\n\nfunction handleRhizohVoiceLiveStartV0(socket, payload = {}) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const sessionId = String(p.sessionId || `live_${Date.now().toString(36)}`).slice(0, 96);\n rhizohVoiceLiveSessions.set(socket, {\n sessionId,\n traceId: String(p.traceId || \"\").slice(0, 128),\n mimeType: String(p.mimeType || \"audio/webm\").slice(0, 80),\n languageCode: String(p.languageCode || \"tr-TR\").slice(0, 32),\n path: String(p.path || \"both\").slice(0, 24),\n chunks: [],\n chunkIndexes: [],\n totalBytes: 0,\n nextChunkIndex: 1,\n eventSeq: 0,\n events: [],\n startedAtMs: Date.now()\n });\n appendRhizohVoiceLiveEventV0(rhizohVoiceLiveSessions.get(socket), \"VOICE_LIVE_SESSION_STARTED\", `${sessionId}:live_session`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_START, {\n ok: true,\n sessionId,\n schema: \"castle.rhizoh.voice_live.gateway_lane.v0\"\n });\n}\n\nfunction handleRhizohVoiceLiveChunkV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const sequence = validateRhizohVoiceLiveChunkPayloadV0(payload, {\n expectedChunkIndex: session.nextChunkIndex\n });\n if (!sequence.ok) {\n rhizohVoiceLiveSessions.delete(socket);\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:chunk_reject`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: sequence.error,\n expectedChunkIndex: sequence.expectedChunkIndex,\n chunkIndex: sequence.chunkIndex\n });\n return;\n }\n const decoded = decodeRhizohVoiceLiveChunkV0(payload);\n if (!decoded.ok) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: decoded.error\n });\n return;\n }\n if (\n session.chunks.length >= RHIZOH_VOICE_LIVE_MAX_CHUNKS ||\n session.totalBytes + decoded.buffer.length > RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES\n ) {\n rhizohVoiceLiveSessions.delete(socket);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_audio_too_large\"\n });\n return;\n }\n session.chunks.push(decoded.buffer);\n session.chunkIndexes.push(sequence.chunkIndex);\n session.totalBytes += decoded.buffer.length;\n session.nextChunkIndex += 1;\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_CHUNK_ACCEPTED\", `${session.sessionId}:chunk:${sequence.chunkIndex}`);\n}\n\nfunction handleRhizohVoiceLiveStopV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n rhizohVoiceLiveSessions.delete(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const lastChunkIndex = Number(payload?.lastChunkIndex);\n if (Number.isInteger(lastChunkIndex) && lastChunkIndex !== session.chunks.length) {\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:stop_mismatch`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_chunk_count_mismatch\",\n expectedChunkCount: lastChunkIndex,\n receivedChunkCount: session.chunks.length\n });\n return;\n }\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_STOP_RECEIVED\", `${session.sessionId}:stop:${session.chunks.length}`);\n const audio = Buffer.concat(session.chunks);\n if (!audio.length) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"audio_empty\"\n });\n return;\n }\n runRhizohVoiceTranscribeV3({\n path: String(payload?.path || session.path || \"both\"),\n audioBase64: audio.toString(\"base64\"),\n mimeType: String(payload?.mimeType || session.mimeType || \"audio/webm\"),\n languageCode: String(payload?.languageCode || session.languageCode || \"tr-TR\"),\n traceId: String(payload?.traceId || session.traceId || \"\"),\n sessionId: session.sessionId,\n transport: \"gateway_ws_live\"\n })\n .then((result) => {\n appendRhizohVoiceLiveEventV0(session, result?.ok ? \"VOICE_TRANSCRIBE_FINAL\" : \"VOICE_TRANSCRIBE_FAILED\", `${session.sessionId}:final`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_FINAL, {\n ...result,\n sessionId: session.sessionId,\n transportPath: \"gateway_ws_live\",\n bytes: audio.length,\n chunkCount: session.chunks.length,\n chunkSequence: Object.freeze({\n ok: true,\n firstChunkIndex: session.chunkIndexes[0] || null,\n lastChunkIndex: session.chunkIndexes[session.chunkIndexes.length - 1] || null,\n receivedChunkCount: session.chunks.length\n }),\n eventTrace: Object.freeze((session.events || []).slice(-16)),\n recordedMs: Math.max(0, Date.now() - session.startedAtMs)\n });\n })\n .catch((e) => {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_transcribe_failed\",\n detail: String(e?.message || e)\n });\n });\n}\n\nfunction removeCastleSocialClientFromAllRooms(clientId) {\n const cid = String(clientId || \"\");\n if (!cid) return;\n for (const room of castleSocialRoomState.values()) {\n room.members.delete(cid);\n }\n}\n\nfunction buildCastleSocialRoomPayload(roomKey) {\n const room = castleSocialRoomState.get(roomKey);\n if (!room) return null;\n const now = Date.now();\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const roster = [...room.members.values()].map((m) => ({\n userId: m.userId,\n lastMs: m.lastMs,\n ...m.sp\n }));\n const peerFeeds =\n room.walPeers && room.walPeers.size\n ? [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }))\n : [];\n return { castleRoomKey: roomKey, seq: room.seq, roster, peerFeeds, ts: now };\n}\n\nconst CASTLE_WAL_PEER_MAX_FEED_BYTES = 48 * 1024;\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {{ ok: boolean, code?: string, reason?: string }} gate\n */\nfunction rejectWalPeerIngressV0(socket, gate) {\n const code = gate.code || \"wal_peer_rejected\";\n recordWalPeerRejectV0(code);\n recordSubstrateOtelEventV0(\"wal_peer_reject\", { code, reason: String(gate.reason || \"\").slice(0, 120) });\n socket.send(\n JSON.stringify(\n createEnvelope(WS_MESSAGE.ERROR, {\n error: gate.reason || \"WAL peer ingress rejected.\",\n code\n })\n )\n );\n}\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {Record<string, unknown> | null | undefined} walPeerFeed\n * @returns {boolean}\n */\nfunction gateWalPeerIngressV0(socket, walPeerFeed) {\n const authGate = assertWalPeerSocketAuthorityV0(socket);\n if (!authGate.ok) {\n rejectWalPeerIngressV0(socket, authGate);\n return false;\n }\n const sigGate = validateWalPeerFeedSignatureV0(\n walPeerFeed && typeof walPeerFeed === \"object\" ? walPeerFeed : {}\n );\n if (!sigGate.ok) {\n rejectWalPeerIngressV0(socket, sigGate);\n return false;\n }\n return true;\n}\n\nfunction handleCastleWalPeerFeed(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_WAL_PEER_MAX_FEED_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_WAL_PEER_FEED too large.\" })));\n return;\n }\n const walPeerFeed = p.walPeerFeed && typeof p.walPeerFeed === \"object\" ? p.walPeerFeed : {};\n if (!gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const castleRoomKey = String(p.castleRoomKey || \"wal:convergence\").slice(0, 64);\n const castleId = String(p.castleId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId,\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: room.members.get(socket.clientId)?.sp ?? {}\n });\n const peerFeeds = [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }));\n const body = {\n castleRoomKey,\n seq: room.seq,\n peerFeeds,\n ts: now\n };\n const env = createEnvelope(WS_MESSAGE.CASTLE_WAL_PEER_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys(), ...room.walPeers.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction handleCastleSocialPulse(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_SOCIAL_MAX_PULSE_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_SOCIAL_PULSE too large.\" })));\n return;\n }\n const castleRoomKey = String(p.castleRoomKey || \"default\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n const sp = p.socialPulse && typeof p.socialPulse === \"object\" ? p.socialPulse : {};\n const walPeerFeed =\n p.walPeerFeed && typeof p.walPeerFeed === \"object\"\n ? p.walPeerFeed\n : sp.walPeerFeed && typeof sp.walPeerFeed === \"object\"\n ? sp.walPeerFeed\n : null;\n if (walPeerFeed && !gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const spNext = walPeerFeed ? { ...sp, walPeerFeed } : { ...sp };\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: spNext\n });\n if (walPeerFeed) {\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId: String(p.castleId || userId).slice(0, 64),\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n }\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const body = buildCastleSocialRoomPayload(castleRoomKey);\n if (!body) return;\n const env = createEnvelope(WS_MESSAGE.CASTLE_SOCIAL_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcast(msgObj) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcastState() {\n broadcast(\n createEnvelope(WS_MESSAGE.BROADCAST_STATE, {\n broadcasterClientId,\n viewers: [...wss.clients].filter((c) => c.readyState === 1).length\n })\n );\n}\n\nfunction currentSpiralStatePayload() {\n return {\n rooms: spiralState.rooms,\n characters: [...spiralState.characters.values()]\n };\n}\n\nfunction broadcastSpiralState() {\n broadcast(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload()));\n}\n\nfunction ensureRoom(roomId, fallbackName = null) {\n if (!roomId) return spiralState.rooms[0].id;\n const exists = spiralState.rooms.find((r) => r.id === roomId);\n if (exists) return exists.id;\n const created = { id: roomId, name: fallbackName || `Spiral-${roomId.slice(-4)}` };\n spiralState.rooms.push(created);\n return created.id;\n}\n\nfunction syncSpiralCharactersWithWorld(snapshot) {\n const agents = snapshot?.agents || [];\n for (const a of agents) {\n if (!spiralState.characters.has(a.id)) {\n spiralState.characters.set(a.id, {\n id: a.id,\n name: a.id,\n role: \"agent\",\n roomId: spiralState.rooms[0].id,\n source: \"world\"\n });\n }\n }\n}\n\nfunction broadcastExcept(msgObj, exceptSocket) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client !== exceptSocket && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction validateInputFrame(payload, socket) {\n const stats = clientStats.get(socket.clientId) || { lastFrame: 0, windowStart: Date.now(), msgCount: 0 };\n const now = Date.now();\n if (now - stats.windowStart > 5000) {\n stats.windowStart = now;\n stats.msgCount = 0;\n }\n stats.msgCount += 1;\n clientStats.set(socket.clientId, stats);\n if (stats.msgCount > 120) return \"Rate limit exceeded.\";\n\n if (!payload || !Number.isInteger(payload.frame)) return \"frame must be integer.\";\n if (payload.frame <= stats.lastFrame) return \"frame must be monotonic.\";\n const commands = Array.isArray(payload.commands) ? payload.commands : [];\n if (commands.length > 8) return \"Too many commands in one frame.\";\n for (const cmd of commands) {\n if (!cmd || typeof cmd.command !== \"string\") return \"Invalid command format.\";\n if (\n cmd.command !== \"SPAWN_AGENT\" &&\n cmd.command !== \"LIST_CASTLES\" &&\n cmd.command !== \"SPAWN_ENTITY\" &&\n cmd.command !== \"CREATE_CASTLE\"\n ) return \"Unsupported command.\";\n }\n stats.lastFrame = payload.frame;\n clientStats.set(socket.clientId, stats);\n return null;\n}\n\nfunction validateSignalPayload(payload) {\n if (!payload || typeof payload !== \"object\") return \"Invalid signaling payload.\";\n const signalType = payload.signalType;\n if (![\"OFFER\", \"ANSWER\", \"ICE\"].includes(signalType)) return \"Invalid signal type.\";\n if (typeof payload.to !== \"string\" || payload.to.length < 3) return \"Invalid signal destination.\";\n if (signalType !== \"ICE\" && JSON.stringify(payload.sdp || {}).length > 16 * 1024) return \"SDP too large.\";\n if (signalType === \"ICE\" && JSON.stringify(payload.candidate || {}).length > 6 * 1024) return \"ICE too large.\";\n return null;\n}\n\nfunction sanitizeRawMessage(raw) {\n if (!raw) return null;\n const text = raw.toString();\n if (Buffer.byteLength(text, \"utf8\") > MAX_MESSAGE_BYTES) return null;\n return text;\n}\n\nwss.on(\"connection\", async (socket, req) => {\n const origin = req?.headers?.origin || \"\";\n if (!httpCors.isWebSocketOriginAllowed(origin)) {\n socket.close(1008, \"Origin not allowed\");\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const path = req?.url || \"\";\n const query = new URL(`ws://localhost${path}`).searchParams;\n const token = query.get(\"token\");\n if (!token || token !== REQUIRED_GATEWAY_TOKEN) {\n socket.close(1008, \"Invalid gateway token\");\n return;\n }\n }\n if (REQUIRE_AUTH) {\n const result = await verifyClientToken(req);\n if (!result.ok) {\n const isLocal = origin.includes(\"localhost\") || origin.includes(\"127.0.0.1\") || !origin;\n if (!(ALLOW_DEV_ANON && isLocal)) {\n socket.close(1008, result.reason);\n return;\n }\n socket.auth = { ok: true, kind: \"dev-anon\", user: { uid: \"dev-anon\" } };\n } else {\n socket.auth = result;\n }\n }\n\n socket.clientId = `c-${Math.random().toString(36).slice(2, 8)}`;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.HELLO, { message: \"Castle Gateway online.\", clientId: socket.clientId })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.WORLD_SNAPSHOT, orchestrator.worldSnapshot())));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.BROADCAST_STATE, { broadcasterClientId, viewers: [...wss.clients].filter((c) => c.readyState === 1).length })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload())));\n broadcastExcept(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }), socket);\n\n socket.on(\"message\", (raw) => {\n const safeRaw = sanitizeRawMessage(raw);\n if (!safeRaw) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Message too large or invalid.\" })));\n return;\n }\n const parsed = safeJsonParse(safeRaw);\n if (!parsed?.type) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid message.\" })));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND_TEXT) {\n const canonical = parseTextToCommand(parsed.payload?.text);\n if (!canonical) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Unknown command text.\" })));\n return;\n }\n const result = orchestrator.applyCommand(canonical);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND) {\n const result = orchestrator.applyCommand(parsed.payload);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n }\n\n if (parsed.type === WS_MESSAGE.INPUT_FRAME) {\n const err = validateInputFrame(parsed.payload, socket);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const frame = parsed.payload.frame;\n const commands = Array.isArray(parsed.payload?.commands) ? parsed.payload.commands : [];\n orchestrator.queueInputFrame(socket.clientId, frame, commands);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, queued: true, frame })));\n }\n\n if (parsed.type === WS_MESSAGE.SIGNAL) {\n const payload = parsed.payload || {};\n const err = validateSignalPayload(payload);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const signalType = payload.signalType;\n const to = payload.to;\n for (const client of wss.clients) {\n if (client.clientId === to && client.readyState === 1) {\n client.send(JSON.stringify(createEnvelope(WS_MESSAGE.SIGNAL, { ...payload, from: socket.clientId })));\n break;\n }\n }\n }\n\n if (parsed.type === WS_MESSAGE.DRONE_TELEMETRY) {\n const payload = parsed.payload || {};\n const json = JSON.stringify(payload);\n if (Buffer.byteLength(json, \"utf8\") > 48 * 1024) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"DRONE_TELEMETRY payload too large.\" })));\n return;\n }\n broadcastExcept(createEnvelope(WS_MESSAGE.DRONE_TELEMETRY, { ...payload, relayFrom: socket.clientId }), socket);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_START) {\n handleRhizohVoiceLiveStartV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_CHUNK) {\n handleRhizohVoiceLiveChunkV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_STOP) {\n handleRhizohVoiceLiveStopV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_SOCIAL_PULSE) {\n handleCastleSocialPulse(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_WAL_PEER_FEED) {\n handleCastleWalPeerFeed(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.OPEN_DATA_QUERY) {\n queryOpenData(parsed.payload)\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: true, result })));\n })\n .catch((e) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: false, error: e.message })));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_QUERY) {\n runRhizohBrainV2({\n ...(parsed.payload || {}),\n uid: socket?.auth?.user?.uid || \"unknown\"\n })\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, result)));\n })\n .catch(async () => {\n const fallback = await runRhizohBrain(parsed.payload || {}).catch(() => ({\n ok: false,\n reply: \"Rhizoh temporary disturbance. Habitat remains stable.\"\n }));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, fallback)));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.BROADCAST_REGISTER) {\n const role = String(parsed.payload?.role || \"\");\n if (role === \"GENESIS_BROADCAST_AGENT\") {\n broadcasterClientId = socket.clientId;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, registered: role })));\n broadcastState();\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.STUDIO_CUE) {\n // Only registered broadcaster can push global cues.\n if (!broadcasterClientId || socket.clientId !== broadcasterClientId) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Only broadcaster agent can emit STUDIO_CUE.\" })));\n return;\n }\n const cue = parsed.payload || {};\n if (typeof cue !== \"object\") {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid STUDIO_CUE payload.\" })));\n return;\n }\n broadcast(createEnvelope(WS_MESSAGE.STUDIO_CUE, cue));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_JOIN_ROOM) {\n const requested = String(parsed.payload?.roomId || \"\").trim();\n const requestedName = String(parsed.payload?.roomName || \"\").trim();\n const roomId = ensureRoom(requested || `room-${Math.random().toString(36).slice(2, 7)}`, requestedName || null);\n spiralState.activeByClient.set(socket.clientId, roomId);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, roomId })));\n broadcastSpiralState();\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_CREATE_CHARACTER) {\n const name = String(parsed.payload?.name || \"\").trim().slice(0, 32) || `AI-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;\n const role = String(parsed.payload?.role || \"agent\").trim().slice(0, 24) || \"agent\";\n const roomId = ensureRoom(\n String(parsed.payload?.roomId || spiralState.activeByClient.get(socket.clientId) || spiralState.rooms[0].id),\n null\n );\n\n // Character creation always maps to an authoritative world agent.\n const result = orchestrator.applyCommand({\n command: COMMAND.SPAWN_AGENT,\n params: { targetCastleId: \"castle-01\" }\n });\n if (!result?.ok || !result?.data?.id) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Character creation failed.\" })));\n return;\n }\n const id = result.data.id;\n spiralState.characters.set(id, { id, name, role, roomId, source: \"spawned\" });\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, characterId: id, roomId })));\n broadcastSpiralState();\n return;\n }\n });\n\n socket.on(\"close\", () => {\n if (socket.clientId === broadcasterClientId) broadcasterClientId = null;\n clientStats.delete(socket.clientId);\n rhizohVoiceLiveSessions.delete(socket);\n spiralState.activeByClient.delete(socket.clientId);\n removeCastleSocialClientFromAllRooms(socket.clientId);\n broadcast(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }));\n broadcastState();\n });\n});\n\nsetInterval(() => {\n void (async () => {\n try {\n const { frame, snapshot, delta, packedDelta } = orchestrator.tick(1 / 20);\n syncSpiralCharactersWithWorld(snapshot);\n broadcast(createEnvelope(WS_MESSAGE.NETWORK_TICK, { frame }));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_TICK, snapshot));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA, delta));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA_PACKED, packedDelta));\n broadcastSpiralState();\n await runAcademyEventTick();\n const executed = await tickAutomations(Date.now());\n for (const a of executed) {\n await addTranscript(a.uid, {\n source: \"planner\",\n eventType: \"automation-executed\",\n text: `Automation executed: ${a.title} (${a.action})`,\n roomId: \"studio-main\",\n meta: { automationId: a.id, action: a.action, deviceId: a.deviceId || \"\" }\n });\n }\n } catch (e) {\n console.error(\"[GATEWAY] world tick / automations loop error:\", String(e?.message || e));\n }\n })();\n}, 50);\n\n(async () => {\n await rhizohRuntime.telemetry.initOpenTelemetry();\n if (String(process.env.CASTLE_GENESIS_DISK_PERSIST ?? \"\").trim() === \"\") {\n process.env.CASTLE_GENESIS_DISK_PERSIST = \"1\";\n }\n const genesisDiskBoot = await hydrateGenesisContinuityPersistenceBootV0();\n if (!genesisDiskBoot.ok) {\n console.warn(\"[GATEWAY] genesis disk hydrate failed; continuing with in-memory genesis continuity\");\n }\n httpServer.listen(PORT, () => {\n startGenesisCanonicalClock();\n installGenesisCheckpointSurfaceGetter(buildGenesisRuntimeSurfacePayloadLive);\n setGenesisContinuityAfterPublishHook((seq) => {\n noteGenesisCheckpointSeqCommitted(seq);\n noteGenesisContinuitySeqAcceptedV0(seq);\n });\n startGenesisContinuityInfraSampler(buildGenesisRuntimeSurfacePayloadLive, 2000);\n logProductionObservatorySurfaceGuardsV0();\n logGatewaySubstrateAuthorityGuardsV0();\n if (isLlmWorkerEnabledV0()) {\n try {\n const workerBoot = initLlmWorkerAtBootV0();\n console.log(\"[GATEWAY] llm worker boot:\", workerBoot);\n } catch (workerErr) {\n console.error(\"[GATEWAY] llm worker boot failed:\", workerErr);\n }\n }\n console.log(`[GATEWAY] ws/http://localhost:${PORT}`);\n console.log(\n `[GATEWAY] genesis continuity: GET ${rhizohRuntime.routes.genesisRuntime} | POST ${rhizohRuntime.routes.genesisIngress} | SSE ${rhizohRuntime.routes.genesisStream} | replay ${rhizohRuntime.routes.genesisReplay} | diff ${rhizohRuntime.routes.genesisReplayDiff} | equiv ${rhizohRuntime.routes.genesisReplayEquivalence} | analytics ${rhizohRuntime.routes.genesisReplayAnalytics} | evolution ${rhizohRuntime.routes.genesisReplayEvolution}`\n );\n if (String(process.env.CASTLE_GENESIS_OBSERVATORY_LIVE_V01 ?? \"\").trim() === \"1\") {\n console.log(\"GENESIS OBSERVATORY LIVE v0.1 — READ ONLY PHASE STARTED\");\n }\n });\n})().catch((e) => {\n console.error(\"[GATEWAY] fatal boot (before or during listen):\", e);\n process.exit(1);\n});\n"}, "a583f14136983cc6df08942e415bb6ec938f6524": {"t": 1785754146.0260687, "n": 167616, "h": "3e303debafffbe21", "c": "import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport dotenv from \"dotenv\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconsole.log(\"🔥 [GENESIS_BOOT] entry file:\", __filename);\nconsole.log(\"🔥 [GENESIS_BOOT] node env:\", process.env.NODE_ENV);\nconsole.log(\"🔥 [GENESIS_BOOT] pid:\", process.pid);\nconsole.log(\"🧬 [GENESIS_BOOT] cwd:\", process.cwd());\nconsole.log(\"🧬 [GENESIS_BOOT] import.meta.url:\", import.meta.url);\ndotenv.config({ path: path.join(__dirname, \"..\", \".env\") });\ndotenv.config({ path: path.join(__dirname, \"..\", \".env.local\"), override: true });\n\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport { createServer } from \"node:http\";\nimport { createOrchestrator } from \"@castle/orchestrator\";\nimport { parseTextToCommand } from \"@castle/command-dsl\";\nimport {\n WS_MESSAGE,\n COMMAND,\n createEnvelope,\n createRhizohImmutableEventV0,\n createRhizohPayloadRefV0,\n safeJsonParse,\n validateRhizohVoiceLiveChunkPayloadV0\n} from \"@castle/protocol\";\nimport { queryOpenData } from \"./openData.js\";\nimport { verifyClientToken } from \"./auth.js\";\nimport { runRhizohBrain } from \"./rhizohBrain.js\";\nimport { queryRhizohLlm } from \"./rhizohLlmGateway.js\";\nimport { rhizohGatewayTurn } from \"./rhizohGatewayTurn.js\";\nimport {\n enqueueLlmWorkerTaskAsyncV0,\n getLatestLlmWorkerTaskIdV0,\n getLlmWorkerDebugSnapshotV0,\n getLlmWorkerTaskSnapshotV0,\n initLlmWorkerAtBootV0,\n isLlmWorkerAsyncEnabledV0,\n isLlmWorkerEnabledV0,\n runLlmWorkerTaskV0\n} from \"./llmWorkerRuntime.js\";\nimport {\n RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n runRhizohVoiceTranscribeV3,\n rhizohVoiceTranscribeEnvV3\n} from \"./rhizohVoiceTranscribeV3.js\";\nimport { createHttpCorsPolicy, normalizeHttpOrigin, parseAllowedOriginsFromEnv } from \"./httpCorsPolicyV1.js\";\nimport {\n applyLanguagePropagationToLlmPayloadV1,\n applyLanguagePropagationToVoiceBodyV1,\n buildRhizohLanguagePropagationEchoV1,\n parseRhizohLanguagePropagationV1\n} from \"./rhizohLanguagePropagationV1.js\";\nimport { trimRhizohLlmGatewayPayloadV1 } from \"./rhizohLlmPayloadTrimV1.js\";\nimport {\n meshAppendDelta,\n meshContinuityAggregate,\n meshJoin,\n meshLeave,\n meshReplayFromNodeId,\n meshReplayFromSeq,\n meshSnapshot,\n meshSubscribeSse\n} from \"./presenceRoomMesh.js\";\nimport { runRhizohBrainV2 } from \"./rhizohBrainV2.js\";\nimport {\n listConnections,\n createConnection,\n updateConnection,\n deleteConnection,\n setDefaultConnection,\n resolveConnection\n} from \"./llmConnectionsStore.js\";\nimport { checkHttpRateLimit, getHttpClientIp } from \"./castleHttpRateLimit.js\";\nimport {\n submitAbuseReportV0,\n listModerationQueueV0,\n softBlockUserV0,\n removeSoftBlockV0,\n listSoftBlockedUsersV0\n} from \"./ops/moderationMvpV0.js\";\nimport { listRecentAgentSnapshotsV0 } from \"./ops/agentObservabilityV0.js\";\nimport { readAgentContainmentConfigV0 } from \"./ops/agentContainmentV0.js\";\nimport { readCostContainmentConfigV0, getCostContainmentStatsV0 } from \"./ops/costContainmentV0.js\";\nimport { getGlobalCostLedgerSnapshotV0, getGclBackendStatusV0 } from \"./ops/globalCostLedgerV0.js\";\nimport { evaluateEconomicStrategyL3V0 } from \"./ops/economicStrategyEngineL3V0.js\";\nimport { getPhasedRolloutStatsV0, readPhasedRolloutTierV0 } from \"./ops/phasedRolloutV0.js\";\nimport { buildUnifiedStateNarrativeV0 } from \"./ops/unifiedStateNarrativeV0.js\";\nimport { resolveHardeningTenantIdV0 } from \"./ops/narrativeTenantIsolationV0.js\";\nimport {\n classifyStressResponseV0,\n verifyStressTaxonomyCoverageV0,\n verifyStressConflictResolutionV0\n} from \"./ops/stressResponseTaxonomyV0.js\";\nimport { verifyResolutionStabilityEnvelopeV0 } from \"./ops/resolutionStabilityEnvelopeV0.js\";\nimport { logLlmAccess } from \"./castleLlmAudit.js\";\nimport { appendMemory, listMemories, getMemoryContext, getPersonaGoalMemory, setPersonaGoalMemory, autoCompactMemories } from \"./memoryStore.js\";\nimport { getFirebasePersistence } from \"./firebasePersistence.js\";\nimport { registerAgentIdentity, listAgentIdentities, getAgentIdentity, updateAgentIdentity } from \"./agentIdentityStore.js\";\nimport { queueAcademyEvent, listAcademyEvents, resolveAcademyEvent, runAcademyEventTick } from \"./academyEventEngine.js\";\nimport { enqueuePdfJob, getPdfJob } from \"./pdfPipeline.js\";\nimport {\n createPublishSession,\n listPublishSessions,\n updatePublishSession,\n addTranscript,\n listTranscripts,\n searchMetadata,\n registerDevice,\n listDevices,\n updateDevice,\n appendTelemetry,\n listTelemetry,\n createAutomation,\n listAutomations,\n tickAutomations,\n enqueueCommand,\n listCommands,\n updateCommandStatus,\n registerSocialChannel,\n listSocialChannels\n} from \"./studioOpsStore.js\";\nimport { metrics as infraMetrics } from \"./infra/metrics.js\";\nimport { renderRhizohPrometheusMetrics, rhizohEnterpriseMetrics } from \"./infra/rhizohEnterpriseMetrics.js\";\nimport { scoreHealth } from \"./infra/healthScore.js\";\nimport { getTraceById, listTracesBySession } from \"./infra/traceRegistry.js\";\nimport { canonicalEpistemicSealString, hashAndSignEpistemicSeal, EPISTEMIC_SEAL_SCHEMA } from \"./epistemicSeal.js\";\nimport { persistEpistemicLedgerBatch } from \"./epistemicLedgerStore.js\";\nimport { persistEpistemicForecastBatch } from \"./epistemicForecastStore.js\";\nimport { buildRhizohExternalGroundTruthPayload } from \"./rhizohExternalGroundTruthGateway.js\";\nimport { ingestRhizohExternalLossBatchHttp } from \"./rhizohExternalLossIngestGateway.js\";\nimport {\n ingestRhizohProductOutcomeHttp,\n listRhizohProductOutcomeAggregates,\n verifyRhizohOutcomeHmac,\n verifyRhizohOutcomeSourceToken\n} from \"./rhizohProductOutcomeIngestGateway.js\";\nimport { initRhizoh } from \"./rhizohProductionBootstrap.js\";\nimport { handleAcademicObservatoryExportGetV0 } from \"./rhizoh/academicObservatoryHttpV0.js\";\nimport {\n handleLiveSportsBundleGetV0,\n handleLiveNewsHeadlinesGetV0,\n handleLiveWorldFeedGetV0\n} from \"./rhizoh/liveWorldFeedHttpV0.js\";\nimport {\n buildGenesisRuntimeSurfacePayload,\n recordGenesisEpistemicLedgerPersisted,\n recordGenesisEpistemicSealIssued,\n startGenesisCanonicalClock\n} from \"./genesisRuntimeSurfaceV0.js\";\nimport { registerGenesisSseClient, getGenesisContinuitySeq, setGenesisContinuityAfterPublishHook } from \"./genesisContinuityStreamHubV0.js\";\nimport { computeGenesisReplayFingerprintV0 } from \"./genesisReplayFingerprintV0.js\";\nimport { buildGenesisGatewayCapabilitiesV0 } from \"./genesisGatewayCapabilitiesV0.js\";\nimport {\n installGenesisCheckpointSurfaceGetter,\n noteGenesisCheckpointSeqCommitted,\n getLatestGenesisSignedCheckpoint\n} from \"./genesisContinuityCheckpointV0.js\";\nimport { hydrateGenesisContinuityPersistenceBootV0 } from \"./genesisContinuityHydrateBootV0.js\";\nimport { startGenesisContinuityInfraSampler } from \"./genesisContinuityInfraSamplerV0.js\";\nimport {\n genesisCheckpointQueryBySeqV0,\n genesisCheckpointQueryRangeV0,\n genesisCheckpointQueryLineageV0\n} from \"./genesisCheckpointQueryV0.js\";\nimport {\n genesisContinuityEventArchiveEnabled,\n queryGenesisContinuityEventArchiveV0,\n GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION\n} from \"./genesisContinuityEventArchiveV0.js\";\nimport { resolveGenesisReplayRouterV1, GENESIS_REPLAY_ROUTER_SCHEMA } from \"./genesisReplayRouterV1.js\";\nimport {\n computeGenesisReplayTemporalDiffV1,\n GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA\n} from \"./genesisReplayTemporalDiffV1.js\";\nimport { compareGenesisReplayEquivalenceV1 } from \"./genesisReplayEquivalenceV1.js\";\nimport { computeGenesisReplayAnalyticsV1 } from \"./genesisReplayAnalyticsV1.js\";\nimport { computeGenesisReplayEvolutionV1 } from \"./genesisReplayEvolutionV1.js\";\nimport { handleStripeCheckoutCreate, handleStripeMembershipWebhook } from \"./stripeWebhookMembership.js\";\nimport { buildRealityHealthPayload } from \"./edgeRealityConsistencyV1.js\";\nimport {\n assertWalPeerSocketAuthorityV0,\n buildGatewaySubstrateAuthoritySnapshotV0,\n logGatewaySubstrateAuthorityGuardsV0,\n validateWalPeerFeedSignatureV0\n} from \"./gatewaySubstrateAuthorityV0.js\";\nimport {\n buildSubstrateOperationalSnapshotV0,\n recordWalPeerRejectV0,\n renderSubstratePrometheusMetricsV0\n} from \"./infra/substrateOperationalMetrics.js\";\nimport { ingestClientSubstrateHealthV0 } from \"./rhizohSubstrateHealthIngestV0.js\";\nimport { recordSubstrateOtelEventV0 } from \"./infra/opentelemetrySubstrateV0.js\";\nimport {\n GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n getGenesisWorldObservationIngressStatusV0,\n ingestGenesisWorldObservationV0\n} from \"./genesisContinuityClientIngressV0.js\";\nimport {\n getGenesisContinuitySeqAuditV0,\n noteGenesisContinuitySeqAcceptedV0\n} from \"./genesisContinuitySeqGapV0.js\";\n\n/** Render / Fly / Railway: `PORT` — yoksa `CASTLE_GATEWAY_PORT` — yoksa 8090. */\nconst PORT =\n Number(process.env.PORT) ||\n Number(process.env.CASTLE_GATEWAY_PORT) ||\n 8090;\n/** Spiral / world WebSocket server — set after `wss` init; used by genesis runtime surface. */\nlet spiralWssForGenesis = null;\nconst MAX_MESSAGE_BYTES = Number(process.env.CASTLE_MAX_MESSAGE_BYTES || 32 * 1024);\nconst REQUIRED_GATEWAY_TOKEN = process.env.CASTLE_GATEWAY_TOKEN || \"\";\nconst ALLOWED_ORIGINS = parseAllowedOriginsFromEnv().fromList;\nconst httpCors = createHttpCorsPolicy();\n\n/** @returns {string|null} `Access-Control-Allow-Origin`; null = browser CORS fail. */\nfunction accessControlAllowOriginValue(req) {\n return httpCors.accessControlAllowOriginValue(req);\n}\n\nfunction applyHttpCorsHeaders(req, res) {\n return httpCors.applyHttpCorsHeaders(req, res);\n}\nconst REQUIRE_AUTH = process.env.CASTLE_REQUIRE_AUTH === \"true\";\nconst ALLOW_DEV_ANON = process.env.CASTLE_ALLOW_DEV_ANON !== \"false\";\nconst ALLOW_DEV_HTTP_UID = process.env.CASTLE_ALLOW_DEV_HTTP_UID !== \"false\";\n/** Presence mesh: allow `X-Castle-Guest-Id` when Firebase auth absent (default on). Set `false` to require auth only. */\nconst ALLOW_MESH_GUEST = process.env.CASTLE_ALLOW_MESH_GUEST !== \"false\";\nconst SOCIAL_RETRY_MAX = Math.max(0, Math.min(5, Number(process.env.CASTLE_SOCIAL_RETRY_MAX || 2)));\nconst SOCIAL_RETRY_BASE_MS = Math.max(100, Number(process.env.CASTLE_SOCIAL_RETRY_BASE_MS || 700));\nconst TELEGRAM_BOT_TOKEN = process.env.CASTLE_TELEGRAM_BOT_TOKEN || \"\";\nconst WHATSAPP_TOKEN = process.env.CASTLE_WHATSAPP_TOKEN || \"\";\nconst WHATSAPP_PHONE_NUMBER_ID = process.env.CASTLE_WHATSAPP_PHONE_NUMBER_ID || \"\";\nconst RL_RHIZOH_LLM_PER_MIN = Math.max(5, Number(process.env.CASTLE_RL_RHIZOH_LLM_PER_MIN || 40));\nconst RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN || 30)\n);\nconst RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN || 120));\nconst RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN = Math.max(\n 10,\n Number(process.env.CASTLE_RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN || 60)\n);\nconst RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN = Math.max(8, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN || 48));\nconst RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN = Math.max(4, Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN || 24));\nconst RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN || 18)\n);\nconst RL_EPISTEMIC_SEAL_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_EPISTEMIC_SEAL_PER_MIN || 120));\nconst RL_EPISTEMIC_LOG_PER_MIN = Math.max(20, Number(process.env.CASTLE_RL_EPISTEMIC_LOG_PER_MIN || 240));\nconst EPISTEMIC_SEAL_SECRET = String(\n process.env.CASTLE_EPISTEMIC_SEAL_SECRET || process.env.CASTLE_GATEWAY_TOKEN || \"\"\n).trim();\nconst RL_LLM_CONN_TEST_PER_MIN = Math.max(2, Number(process.env.CASTLE_RL_LLM_CONNECTION_TEST_PER_MIN || 8));\n\n/** One-shot production warnings: CORS surface, auth toggle, dev bypass flags (observatory deploy hygiene). */\nfunction logProductionObservatorySurfaceGuardsV0() {\n if (String(process.env.NODE_ENV || \"\").toLowerCase() !== \"production\") return;\n const tag = \"[GATEWAY_PROD_GUARD]\";\n const primaryCors = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n const corsListEmpty = ALLOWED_ORIGINS.length === 0;\n if (corsListEmpty && !primaryCors) {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty and CASTLE_HTTP_CORS_ORIGIN empty — production still merges RHIZOH_BROWSER_ORIGINS_V1 defaults; set env for preview/staging hosts`\n );\n } else if (corsListEmpty && primaryCors && primaryCors !== \"*\") {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty — only CASTLE_HTTP_CORS_ORIGIN set; add web.app + firebaseapp.com (and previews) to CASTLE_ALLOWED_ORIGINS if clients use alternate hostnames`\n );\n }\n if (primaryCors && /localhost|127\\.0\\.0\\.1/i.test(primaryCors)) {\n console.warn(`${tag} CORS: CASTLE_HTTP_CORS_ORIGIN is localhost while NODE_ENV=production`);\n }\n if (REQUIRE_AUTH) {\n console.warn(\n `${tag} AUTH: CASTLE_REQUIRE_AUTH=true — clients must send a valid gateway credential (otherwise silent 401 on replay/SSE); EventSource cannot set custom headers unless proxied`\n );\n if (REQUIRED_GATEWAY_TOKEN.length < 16) {\n console.warn(`${tag} AUTH: CASTLE_GATEWAY_TOKEN missing or short while require-auth enabled`);\n }\n }\n if (ALLOW_DEV_ANON) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_ANON is not \"false\" — dev anonymous path may be live in production (set CASTLE_ALLOW_DEV_ANON=false)`\n );\n }\n if (ALLOW_DEV_HTTP_UID) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_HTTP_UID is not \"false\" — X-Castle-Dev-Uid bypass risk (set CASTLE_ALLOW_DEV_HTTP_UID=false)`\n );\n }\n}\n\nconst rhizohRuntime = initRhizoh();\n\n/** İstemci: llmKeySource veya keySource — env | user_connection | auto */\nfunction normalizeClientLlmKeySource(raw) {\n const s = String(raw ?? \"\").trim().toLowerCase();\n if (s === \"env\" || s === \"server\") return \"env\";\n if (s === \"user_connection\" || s === \"connection\" || s === \"user\") return \"user_connection\";\n if (s === \"auto\" || s === \"\") return \"auto\";\n return null;\n}\n\nfunction sendJson(res, statusCode, payload) {\n res.writeHead(statusCode, { \"Content-Type\": \"application/json; charset=utf-8\" });\n res.end(JSON.stringify(payload));\n}\n\nfunction logRhizohHealth(stage, detail = {}) {\n try {\n console.info(`[CASTLE_${String(stage || \"unknown\")}]`, detail && typeof detail === \"object\" ? detail : {});\n } catch {\n /* noop */\n }\n}\n\nfunction renderInfraMetricsProm() {\n const lines = [\n `castle_gateway_events_processed_total ${infraMetrics.eventsProcessed}`,\n `castle_gateway_duplicate_reject_total ${infraMetrics.duplicateRejects}`,\n `castle_gateway_enqueue_latency_ms ${infraMetrics.enqueueLatencyMs}`,\n `castle_gateway_queue_depth ${infraMetrics.queueDepth}`,\n `castle_gateway_queue_lag_seconds ${(Number(infraMetrics.queueLag || 0) / 1000).toFixed(3)}`,\n `castle_gateway_errors_total ${infraMetrics.errors}`\n ];\n const buckets = infraMetrics.enqueueLatencyBuckets || {};\n for (const k of Object.keys(buckets)) {\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"${k}\"} ${buckets[k]}`);\n }\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"+Inf\"} ${infraMetrics.eventsProcessed}`);\n if (process.env.CASTLE_SUBSTRATE_METRICS !== \"0\") {\n lines.push(renderSubstratePrometheusMetricsV0().trimEnd());\n }\n return lines.join(\"\\n\");\n}\n\nasync function fetchJsonWithTimeout(url, timeoutMs = 1200) {\n const ctl = new AbortController();\n const tid = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const res = await fetch(url, { signal: ctl.signal });\n if (!res.ok) return null;\n return await res.json();\n } catch {\n return null;\n } finally {\n clearTimeout(tid);\n }\n}\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction getHttpPathname(req) {\n let p = String(req.url || \"\").split(\"?\")[0];\n /* Reverse proxies / clients sometimes append `/` — avoid silent 404 on e.g. `/rhizoh/genesis/stream/`. */\n while (p.length > 1 && p.endsWith(\"/\")) {\n p = p.slice(0, -1);\n }\n return p;\n}\n\nfunction rhizohLlmEnvConfigured() {\n const p = String(process.env.CASTLE_LLM_PROVIDER || \"openai\").toLowerCase();\n const key =\n p === \"anthropic\"\n ? process.env.ANTHROPIC_API_KEY\n : p === \"gemini\"\n ? process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY\n : p === \"xai\"\n ? process.env.XAI_API_KEY\n : p === \"deepseek\"\n ? process.env.DEEPSEEK_API_KEY\n : p === \"mistral\"\n ? process.env.MISTRAL_API_KEY\n : p === \"openrouter\"\n ? process.env.OPENROUTER_API_KEY\n : process.env.OPENAI_API_KEY;\n return !!String(key || \"\").trim();\n}\n\n/**\n * @param {import(\"firebase-admin/firestore\").Firestore | null} db\n * @param {number} timeoutMs\n */\nasync function probeFirestoreReachable(db, timeoutMs = 2000) {\n if (!db) return { ok: false, reason: \"no_db\" };\n const timeout = sleep(timeoutMs).then(() => ({ ok: false, reason: \"timeout\" }));\n const work = db\n .collection(\"_castle_gateway_health\")\n .doc(\"ping\")\n .get()\n .then(() => ({ ok: true, reason: \"reachable\" }))\n .catch((e) => ({ ok: false, reason: String(e?.message || e || \"firestore_error\") }));\n return Promise.race([work, timeout]);\n}\n\nfunction stripHtml(text = \"\") {\n return String(text)\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \" \")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \" \")\n .replace(/<[^>]+>/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction haversineMeters(aLat, aLon, bLat, bLon) {\n const R = 6371000;\n const dLat = ((bLat - aLat) * Math.PI) / 180;\n const dLon = ((bLon - aLon) * Math.PI) / 180;\n const sa = Math.sin(dLat / 2);\n const sb = Math.sin(dLon / 2);\n const x = sa * sa + Math.cos((aLat * Math.PI) / 180) * Math.cos((bLat * Math.PI) / 180) * sb * sb;\n return 2 * R * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));\n}\n\nasync function checkPublishTargetHealth(protocol, target) {\n const p = String(protocol || \"\").toUpperCase();\n const t = String(target || \"\").trim();\n if (!t) return { ok: false, reason: \"target_missing\", shouldReconnect: false };\n if (p === \"RTMP\") {\n const ok = t.startsWith(\"rtmp://\") || t.startsWith(\"rtmps://\");\n return { ok, reason: ok ? \"rtmp_target_format_ok\" : \"invalid_rtmp_target\", shouldReconnect: !ok, reconnectInSec: 15 };\n }\n if (p === \"WHIP\" || p === \"SFU\") {\n try {\n const res = await fetch(t, { method: \"OPTIONS\" });\n const ok = res.status < 500;\n return { ok, reason: `http_${res.status}`, shouldReconnect: !ok, reconnectInSec: 8 };\n } catch {\n return { ok: false, reason: \"network_unreachable\", shouldReconnect: true, reconnectInSec: 8 };\n }\n }\n return { ok: true, reason: \"unchecked_protocol\", shouldReconnect: false };\n}\n\nasync function fetchTextFromUrl(url) {\n const res = await fetch(url, { method: \"GET\" });\n if (!res.ok) throw new Error(`fetch_${res.status}`);\n const ct = String(res.headers.get(\"content-type\") || \"\");\n if (ct.includes(\"application/pdf\")) throw new Error(\"pdf_binary_not_supported_directly\");\n const body = await res.text();\n return stripHtml(body).slice(0, 14000);\n}\n\nfunction buildTaskActionGraph(task = \"\", context = {}) {\n const t = String(task || \"\").toLowerCase();\n if (t.includes(\"dinner\") || t.includes(\"akşam yemeği\") || t.includes(\"aksam yemegi\")) {\n return {\n taskTitle: \"Dinner Prep Planner\",\n nodes: [\n { id: \"n1\", title: \"Kitchen preheat\", action: \"smart-oven.preheat\", params: { temperatureC: 190, mode: \"fan\" } },\n { id: \"n2\", title: \"Lighting comfort\", action: \"smart-light.set_scene\", params: { scene: \"warm_dinner\" } },\n { id: \"n3\", title: \"Fridge inventory check\", action: \"smart-fridge.inventory_scan\", params: {} },\n { id: \"n4\", title: \"Reminder notification\", action: \"smart-watch.notify\", params: { text: \"Dinner prep started.\" } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" },\n { from: \"n3\", to: \"n4\" }\n ]\n };\n }\n return {\n taskTitle: task || \"General Planner\",\n nodes: [\n { id: \"n1\", title: \"Safety check\", action: \"device.safety_check\", params: { context } },\n { id: \"n2\", title: \"Primary action\", action: \"device.execute_task\", params: { task } },\n { id: \"n3\", title: \"Report\", action: \"studio.timeline_note\", params: { task } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" }\n ]\n };\n}\n\nfunction buildEthicsPrograms() {\n return {\n policyVersion: \"v1\",\n tracks: [\n {\n id: \"elderly-care-assist\",\n audience: \"elderly\",\n title: \"Elderly Assistance Track\",\n principles: [\"human-in-the-loop\", \"consent-first\", \"low-friction alerts\"],\n modules: [\"medication-reminder\", \"fall-risk-check\", \"family-notify\", \"daily-companion-brief\"]\n },\n {\n id: \"kids-safe-learning\",\n audience: \"children\",\n title: \"Kids Safe Learning Track\",\n principles: [\"guardian-consent\", \"age-appropriate-language\", \"content-filtering\"],\n modules: [\"study-coach\", \"screen-time-balance\", \"safe-social-share\", \"emergency-contact\"]\n }\n ]\n };\n}\n\nasync function deliverViaTelegram(channel, text) {\n const chatId = String(channel.endpoint || \"\").trim();\n if (!TELEGRAM_BOT_TOKEN) throw new Error(\"telegram_token_missing\");\n if (!chatId) throw new Error(\"telegram_chat_id_missing\");\n const url = `https://api.telegram.org/bot${encodeURIComponent(TELEGRAM_BOT_TOKEN)}/sendMessage`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ chat_id: chatId, text })\n });\n if (!res.ok) throw new Error(`telegram_http_${res.status}`);\n return { provider: \"telegram\", status: res.status };\n}\n\nasync function deliverViaWhatsapp(channel, text) {\n const to = String(channel.endpoint || \"\").trim();\n if (!WHATSAPP_TOKEN) throw new Error(\"whatsapp_token_missing\");\n if (!WHATSAPP_PHONE_NUMBER_ID) throw new Error(\"whatsapp_phone_number_id_missing\");\n if (!to) throw new Error(\"whatsapp_to_missing\");\n const url = `https://graph.facebook.com/v22.0/${encodeURIComponent(WHATSAPP_PHONE_NUMBER_ID)}/messages`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${WHATSAPP_TOKEN}` },\n body: JSON.stringify({ messaging_product: \"whatsapp\", to, type: \"text\", text: { body: text } })\n });\n if (!res.ok) throw new Error(`whatsapp_http_${res.status}`);\n return { provider: \"whatsapp\", status: res.status };\n}\n\nasync function deliverViaWebhook(channel, text) {\n const endpoint = String(channel.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"webhook_endpoint_missing\");\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text, platform: channel.platform || \"webhook\", ts: Date.now() })\n });\n if (!res.ok) throw new Error(`webhook_http_${res.status}`);\n return { provider: \"webhook\", status: res.status };\n}\n\nasync function deliverSocialMessage(channel, text) {\n const platform = String(channel.platform || \"webhook\").toLowerCase();\n if (platform === \"telegram\") return deliverViaTelegram(channel, text);\n if (platform === \"whatsapp\") return deliverViaWhatsapp(channel, text);\n return deliverViaWebhook(channel, text);\n}\n\nasync function deliverSocialWithRetry(channel, text) {\n let attempt = 0;\n let lastError = null;\n while (attempt <= SOCIAL_RETRY_MAX) {\n try {\n const result = await deliverSocialMessage(channel, text);\n return { ok: true, attempt: attempt + 1, ...result };\n } catch (error) {\n lastError = error;\n if (attempt >= SOCIAL_RETRY_MAX) break;\n const jitter = Math.floor(Math.random() * 90);\n await sleep(SOCIAL_RETRY_BASE_MS * Math.pow(2, attempt) + jitter);\n }\n attempt += 1;\n }\n return { ok: false, attempt: attempt + 1, error: String(lastError?.message || \"delivery_failed\") };\n}\n\nasync function dispatchCommandToAdapter(device, command) {\n const adapter = String(command?.adapter || device?.adapter || \"websocket\").toLowerCase();\n const endpoint = String(device?.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"device_endpoint_missing\");\n if (adapter === \"mqtt\") {\n // MQTT delivery is proxied via a bridge endpoint (HTTP webhook style).\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n type: \"mqtt-command\",\n topic: `castle/devices/${device.id}/commands`,\n action: command.action,\n params: command.params || {}\n })\n });\n if (!res.ok) throw new Error(`mqtt_bridge_http_${res.status}`);\n return { ok: true, adapter, via: \"http-bridge\", status: res.status };\n }\n if (!endpoint.startsWith(\"ws://\") && !endpoint.startsWith(\"wss://\")) throw new Error(\"invalid_websocket_endpoint\");\n const sendResult = await new Promise((resolve, reject) => {\n const ws = new WebSocket(endpoint);\n const timer = setTimeout(() => {\n try {\n ws.close();\n } catch {\n /* noop */\n }\n reject(new Error(\"websocket_timeout\"));\n }, 3000);\n ws.on(\"open\", () => {\n ws.send(\n JSON.stringify({\n type: \"castle-command\",\n commandId: command.id,\n deviceId: command.deviceId,\n action: command.action,\n params: command.params || {}\n }),\n (err) => {\n clearTimeout(timer);\n ws.close();\n if (err) reject(new Error(\"websocket_send_failed\"));\n else resolve({ ok: true, adapter: \"websocket\", via: \"ws\" });\n }\n );\n });\n ws.on(\"error\", () => {\n clearTimeout(timer);\n reject(new Error(\"websocket_connect_failed\"));\n });\n });\n return sendResult;\n}\n\nfunction readHttpJson(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => {\n if (!buf) return resolve({});\n const payload = safeJsonParse(buf);\n if (!payload || typeof payload !== \"object\") return reject(new Error(\"invalid_json_payload\"));\n resolve(payload);\n });\n req.on(\"error\", reject);\n });\n}\n\n/** Ham gövde (HMAC doğrulaması için). */\nfunction readHttpBodyText(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => resolve(buf));\n req.on(\"error\", reject);\n });\n}\n\nfunction maskOpaqueId(value) {\n const t = String(value || \"\");\n if (!t) return \"(empty)\";\n if (t.length <= 8) return \"****\";\n return `${t.slice(0, 4)}…${t.slice(-2)}`;\n}\n\nfunction readGatewayHttpToken(req) {\n const x = String(req.headers?.[\"x-castle-gateway-token\"] || \"\").trim();\n if (x) return x;\n const auth = String(req.headers?.authorization || \"\");\n const m = /^Bearer\\s+(.+)$/i.exec(auth);\n return m ? String(m[1] || \"\").trim() : \"\";\n}\n\nasync function resolveHttpUser(req) {\n const authResult = await verifyClientToken(req);\n if (authResult?.ok && authResult.user?.uid) {\n return { ok: true, uid: String(authResult.user.uid), mode: authResult.kind || \"auth\" };\n }\n if (ALLOW_DEV_ANON && ALLOW_DEV_HTTP_UID) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) return { ok: true, uid: `dev-${devUid.slice(0, 64)}`, mode: \"dev-header\" };\n }\n return { ok: false, reason: authResult?.reason || \"auth_required\" };\n}\n\n/**\n * Epistemic ledger ingest — Firebase uid OR valid transport token + X-Castle-Dev-Uid.\n * Prod disables CASTLE_ALLOW_DEV_HTTP_UID; LLM may still work while telemetry 401'd without this.\n */\nasync function resolveEpistemicIngestActor(req) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok && tok === REQUIRED_GATEWAY_TOKEN) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) {\n return { ok: true, uid: `gw-telemetry-${devUid.slice(0, 64)}`, mode: \"gateway-token-dev-uid\" };\n }\n return { ok: false, reason: \"dev_uid_required\" };\n }\n if (tok) {\n return { ok: false, reason: \"gateway_token_mismatch\" };\n }\n }\n const auth = await resolveHttpUser(req);\n if (auth.ok) return auth;\n return { ok: false, reason: auth.reason || \"auth_required\" };\n}\n\n/** Mesh actor: Firebase uid, dev header, or `guest:<uuid>` via header / `guestId` query (SSE cannot set headers). */\nasync function resolveMeshActor(req) {\n const auth = await resolveHttpUser(req);\n if (auth.ok) return { ok: true, clientUid: auth.uid, mode: auth.mode };\n if (ALLOW_MESH_GUEST) {\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const gid = String(req.headers?.[\"x-castle-guest-id\"] || q.searchParams.get(\"guestId\") || \"\").trim();\n if (gid) {\n const normalized = gid.startsWith(\"guest:\") ? gid.slice(0, 180) : `guest:${gid.slice(0, 160)}`;\n return { ok: true, clientUid: normalized, mode: \"guest-header\" };\n }\n }\n return { ok: false, reason: \"mesh_auth_required\" };\n}\n\n/** Live genesis runtime JSON (GET /rhizoh/genesis/runtime + SSE infra sampler). */\nasync function buildGenesisRuntimeSurfacePayloadLive() {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n let spiralWsActive = 0;\n if (spiralWssForGenesis) {\n for (const c of spiralWssForGenesis.clients) {\n if (c.readyState === 1) spiralWsActive += 1;\n }\n }\n const base = buildGenesisRuntimeSurfacePayload({\n infraMetrics,\n scoredHealth: scored,\n rhizohEnterpriseMetrics,\n mesh: meshContinuityAggregate(),\n workerHealth: worker,\n port: PORT,\n spiralWebSocketClientsActive: spiralWsActive\n });\n return {\n ...base,\n replayFingerprint: computeGenesisReplayFingerprintV0(base),\n gatewayCapabilities: buildGenesisGatewayCapabilitiesV0(),\n genesisStream: {\n schema: \"castle.genesis.stream_cursor.v0\",\n lastAcceptedSeq: getGenesisContinuitySeq(),\n clientIngress: {\n schema: GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n contract: \"castle.world_observation.ingress_envelope.v1\",\n postPath: rhizohRuntime.routes.genesisIngress,\n closure: getGenesisWorldObservationIngressStatusV0()\n },\n seqContinuity: getGenesisContinuitySeqAuditV0(),\n eventArchive: genesisContinuityEventArchiveEnabled()\n ? {\n enabled: true,\n projection: GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION,\n queryPath: rhizohRuntime.routes.genesisContinuityEvents\n }\n : { enabled: false },\n replayRouter: {\n schema: GENESIS_REPLAY_ROUTER_SCHEMA,\n queryPath: rhizohRuntime.routes.genesisReplay,\n queryHint: \"from=&to= or range=from-to; optional type=&checkpoints=0\",\n temporalDiffPath: rhizohRuntime.routes.genesisReplayDiff,\n temporalDiffSchema: GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA,\n equivalencePath: rhizohRuntime.routes.genesisReplayEquivalence,\n analyticsPath: rhizohRuntime.routes.genesisReplayAnalytics,\n evolutionPath: rhizohRuntime.routes.genesisReplayEvolution\n }\n }\n };\n}\n\nconst httpServer = createServer(async (req, res) => {\n const corsAllow = applyHttpCorsHeaders(req, res);\n if (req.method === \"OPTIONS\") {\n if (!corsAllow && req.headers?.origin) {\n console.warn(\"[GATEWAY_CORS] preflight rejected for origin:\", req.headers.origin);\n }\n res.writeHead(corsAllow ? 204 : 403);\n res.end();\n return;\n }\n\n const pathname = getHttpPathname(req);\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/genesis/__ping\") {\n sendJson(res, 200, {\n ok: true,\n pid: process.pid,\n entry: __filename,\n time: Date.now(),\n genesis: true\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/epistemic/__probe\") {\n sendJson(res, 200, {\n ok: true,\n epistemicIngestActorV2: true,\n routes: {\n seal: rhizohRuntime.routes.epistemicSeal,\n logsBatch: rhizohRuntime.routes.epistemicLogsBatch\n },\n hasGatewayToken: Boolean(REQUIRED_GATEWAY_TOKEN),\n gatewayTokenLen: REQUIRED_GATEWAY_TOKEN ? REQUIRED_GATEWAY_TOKEN.length : 0\n });\n return;\n }\n\n if (\n req.method === \"GET\" &&\n (pathname === \"/rhizoh/live/world-feed\" ||\n pathname === \"/rhizoh/live/sports-bundle\" ||\n pathname === \"/rhizoh/live/news-headlines\")\n ) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_live_world_feed:${ip}`, RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const out =\n pathname === \"/rhizoh/live/sports-bundle\"\n ? await handleLiveSportsBundleGetV0()\n : pathname === \"/rhizoh/live/news-headlines\"\n ? await handleLiveNewsHeadlinesGetV0(req)\n : await handleLiveWorldFeedGetV0(req);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/webhooks/stripe\") {\n await handleStripeMembershipWebhook(req, res);\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/billing/stripe/checkout\") {\n await handleStripeCheckoutCreate(req, res, { readHttpJson, resolveHttpUser, sendJson });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/live\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, live: false, dns: true, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n sendJson(res, 200, { ok: true, live: true, dns: true, service: \"castle-gateway\", ts: Date.now() });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/cors\") {\n const reqOrigin = normalizeHttpOrigin(req.headers?.origin);\n const allowOrigin = accessControlAllowOriginValue(req);\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n requestOrigin: reqOrigin || null,\n allowed: Boolean(allowOrigin),\n allowOrigin: allowOrigin || null,\n allowSetSize: httpCors.allowSet.size\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/ready\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, ready: false, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n try {\n const persistence = getFirebasePersistence();\n const llmProbe = rhizohLlmEnvConfigured();\n sendJson(res, 200, {\n ok: true,\n ready: true,\n dns: true,\n persistence: persistence.mode,\n service: \"castle-gateway\",\n gateway: \"ok\",\n llm: llmProbe ? \"ok\" : \"degraded\",\n spine: llmProbe ? \"ok\" : \"partial\"\n });\n } catch (e) {\n sendJson(res, 503, { ok: false, ready: false, reason: String(e?.message || e), service: \"castle-gateway\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/reality\") {\n const payload = buildRealityHealthPayload();\n sendJson(res, payload.ok ? 200 : 503, payload);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/substrate\") {\n const authority = buildGatewaySubstrateAuthoritySnapshotV0();\n const operational = buildSubstrateOperationalSnapshotV0();\n const drift = buildRealityHealthPayload();\n const ok = Boolean(authority.productionAuthoritySatisfied) && drift.ok;\n sendJson(res, ok ? 200 : 503, {\n ok,\n service: \"castle-gateway\",\n ts: Date.now(),\n authority,\n operational,\n drift: { ok: drift.ok, errors: drift.drift?.errors, warnings: drift.drift?.warnings }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/substrate/health\") {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const body = await readHttpJson(req);\n const uid = (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestClientSubstrateHealthV0(body, { uid, clientId: uid });\n if (!result.ok) {\n sendJson(res, 400, result);\n return;\n }\n recordSubstrateOtelEventV0(\"client_health_ingest\", { uid: String(uid).slice(0, 32) });\n sendJson(res, 200, result);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/deps\") {\n const t0 = Date.now();\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, {\n ok: false,\n dns: true,\n llm: false,\n firestore: false,\n phase: \"maintenance\",\n latencyMs: Date.now() - t0,\n service: \"castle-gateway\"\n });\n return;\n }\n const persistence = getFirebasePersistence();\n let firestoreOk = persistence.mode !== \"firebase\";\n let firestoreDetail = persistence.mode === \"file\" ? \"file_mode\" : \"not_applicable\";\n if (persistence.mode === \"firebase\" && persistence.db) {\n const pr = await probeFirestoreReachable(persistence.db, 2000);\n firestoreOk = pr.ok;\n firestoreDetail = pr.reason || (pr.ok ? \"reachable\" : \"fail\");\n } else if (persistence.mode === \"firebase\" && !persistence.db) {\n firestoreOk = false;\n firestoreDetail = \"firebase_not_initialized\";\n }\n const llmOk = rhizohLlmEnvConfigured();\n const overallOk = llmOk && firestoreOk;\n const latencyMs = Date.now() - t0;\n sendJson(res, 200, {\n ok: overallOk,\n dns: true,\n llm: llmOk,\n firestore: persistence.mode === \"firebase\" ? firestoreOk : false,\n persistence: persistence.mode,\n firestoreDetail,\n latencyMs,\n service: \"castle-gateway\",\n wsPort: PORT\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health\") {\n const persistence = getFirebasePersistence();\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n wsPort: PORT,\n persistence: persistence.mode,\n presenceMesh: \"sse\"\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/health\") {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n sendJson(res, 200, {\n ok: true,\n role: \"gateway\",\n queueLagMs: infraMetrics.queueLag,\n queueDepth: infraMetrics.queueDepth,\n errors: infraMetrics.errors,\n status: scored.status,\n score: scored.score,\n reasons: scored.reasons,\n worker: worker\n ? {\n status: worker.status,\n score: worker.score,\n reasons: worker.reasons\n }\n : null,\n timestamp: Date.now()\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisRuntime) {\n const payload = await buildGenesisRuntimeSurfacePayloadLive();\n sendJson(res, 200, payload);\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.genesisIngress) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const actor = await resolveMeshActor(req);\n const clientId = actor.ok ? actor.clientUid : (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestGenesisWorldObservationV0(body, { clientId });\n if (!result.ok) {\n const status =\n result.error === \"rate_limited\" || result.error === \"agent_spoke_throttled\"\n ? 429\n : result.error === \"payload_too_large\"\n ? 413\n : 400;\n sendJson(res, status, result);\n return;\n }\n sendJson(res, result.idempotent ? 200 : 202, result);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisStream) {\n const corsOrigin = accessControlAllowOriginValue(req);\n if (!corsOrigin && String(process.env.NODE_ENV || \"\").toLowerCase() === \"production\") {\n const og = String(req.headers?.origin || \"\").trim();\n if (og) {\n console.warn(\"[GATEWAY] genesis SSE: missing Access-Control-Allow-Origin for browser Origin (CORS whitelist mismatch):\", og);\n }\n }\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n res.write(\"retry: 3000\\n\\n\");\n res.write(\": genesis continuity stream\\n\\n\");\n res.write(\"event: genesis\\n\");\n res.write(`data: {\"booted\":true,\"pid\":${process.pid}}\\n\\n`);\n registerGenesisSseClient(res, req);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLatest) {\n const cp = getLatestGenesisSignedCheckpoint();\n if (!cp) {\n sendJson(res, 404, { ok: false, error: \"no_checkpoint_yet\", hint: \"wait_for_seq_interval_or_configure_signing_secret\" });\n return;\n }\n sendJson(res, 200, { ok: true, checkpoint: cp });\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(rhizohRuntime.routes.genesisCheckpointBySeqPrefix)) {\n const tail = pathname.slice(rhizohRuntime.routes.genesisCheckpointBySeqPrefix.length).split(\"/\")[0];\n const seq = parseInt(String(tail || \"\").trim(), 10);\n const out = await genesisCheckpointQueryBySeqV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"checkpoint_not_found\") {\n sendJson(res, 404, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointRange) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryRangeV0(from, to);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"range_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLineage) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const seq = parseInt(String(u.searchParams.get(\"seq\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryLineageV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"lineage_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisContinuityEvents) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const limit = parseInt(String(u.searchParams.get(\"limit\") || \"\").trim(), 10);\n const out = await queryGenesisContinuityEventArchiveV0(from, to, type, limit);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\" || out.error === \"event_archive_disabled\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"archive_file_too_large\") {\n sendJson(res, 413, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplay) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await resolveGenesisReplayRouterV1({\n from: fromSeq,\n to: toSeq,\n type,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayDiff) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const out = await computeGenesisReplayTemporalDiffV1({ from: fromSeq, to: toSeq, type });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEquivalence) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from1 = parseInt(String(u.searchParams.get(\"from1\") || \"\").trim(), 10);\n const to1 = parseInt(String(u.searchParams.get(\"to1\") || \"\").trim(), 10);\n const from2 = parseInt(String(u.searchParams.get(\"from2\") || \"\").trim(), 10);\n const to2 = parseInt(String(u.searchParams.get(\"to2\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await compareGenesisReplayEquivalenceV1({\n from1,\n to1,\n from2,\n to2,\n type,\n includeCheckpoints\n });\n if (\n !out.ok &&\n (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")\n ) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayAnalytics) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayAnalyticsV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEvolution) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const collapseWindows = parseInt(String(u.searchParams.get(\"collapseWindows\") || \"0\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayEvolutionV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n collapseWindows,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/metrics\") {\n res.writeHead(200, { \"Content-Type\": \"text/plain; version=0.0.4; charset=utf-8\" });\n res.end(renderInfraMetricsProm());\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.infraPrometheusMetrics) {\n if (process.env.CASTLE_PROMETHEUS_METRICS !== \"1\") {\n sendJson(res, 404, { ok: false, error: \"prometheus_metrics_disabled\" });\n return;\n }\n res.writeHead(200, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(renderRhizohPrometheusMetrics());\n return;\n }\n if (req.method === \"GET\" && pathname.startsWith(\"/infra/traces/\")) {\n const traceId = decodeURIComponent(pathname.slice(\"/infra/traces/\".length));\n const item = await getTraceById(traceId);\n if (!item) return sendJson(res, 404, { ok: false, error: \"trace_not_found\" });\n return sendJson(res, 200, { ok: true, trace: item });\n }\n if (req.method === \"GET\" && pathname === \"/infra/traces\") {\n const u = new URL(String(req.url || \"/infra/traces\"), `http://${req.headers.host || \"localhost\"}`);\n const sessionId = String(u.searchParams.get(\"sessionId\") || \"\").trim();\n const items = sessionId ? await listTracesBySession(sessionId) : [];\n return sendJson(res, 200, { ok: true, sessionId, traces: items });\n }\n\n /** Presence room mesh (C): JOIN / LEAVE / SNAPSHOT / DELTA / REPLAY (HTTP); SUBSCRIBE = SSE stream. */\n if (req.method === \"POST\" && pathname === \"/presence/mesh/join\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid, clientUid: actor.clientUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/leave\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshLeave(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/snapshot\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n sendJson(res, 200, meshSnapshot(roomUid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/delta\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, MAX_MESSAGE_BYTES);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const seq = meshAppendDelta(roomUid, actor.clientUid, {\n node: body?.node,\n projectionPatch: body?.projectionPatch,\n writerSubject: body?.writerSubject\n });\n sendJson(res, 200, { ok: true, roomUid, seq });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/replay\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const fromNodeId = body?.fromNodeId != null ? String(body.fromNodeId) : \"\";\n const fromSeq = body?.fromSeq;\n if (fromNodeId) {\n sendJson(res, 200, meshReplayFromNodeId(roomUid, fromNodeId));\n return;\n }\n sendJson(res, 200, meshReplayFromSeq(roomUid, fromSeq));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/presence/mesh/subscribe\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const roomUid = String(q.searchParams.get(\"roomUid\") || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n const write = (chunk) => {\n try {\n res.write(chunk);\n } catch {\n /* closed */\n }\n };\n const unsub = meshSubscribeSse(roomUid, write);\n const onClose = () => {\n unsub();\n meshLeave(roomUid, actor.clientUid);\n try {\n res.end();\n } catch {\n /* ignore */\n }\n };\n req.on(\"close\", onClose);\n req.on(\"aborted\", onClose);\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, uid: auth.uid, mode: auth.mode, items: await listConnections(auth.uid) });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const id = await createConnection(auth.uid, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"create_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n await updateConnection(auth.uid, id, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"update_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"DELETE\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n await deleteConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"delete_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections/test\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n if (!checkHttpRateLimit(`llm_conn_test:${auth.uid}`, RL_LLM_CONN_TEST_PER_MIN, 60_000)) {\n return sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n }\n const hasBodyKey = !!String(payload?.apiKey || \"\").trim();\n const result = await queryRhizohLlm(\n {\n message: String(payload?.message || \"Ping from Castle Gateway\"),\n context: payload?.context || { source: \"connection_test\" },\n provider: payload?.provider,\n model: payload?.model,\n apiKey: payload?.apiKey || \"\"\n },\n { allowExternalApiKey: true, llmKeySource: hasBodyKey ? \"auto\" : \"env\" }\n );\n logLlmAccess({\n route: \"/llm/connections/test\",\n uid: auth.uid,\n llmKeyBillingOwner: result.llmKeyBillingOwner,\n llmKeyOrigin: result.llmKeyOrigin,\n provider: result.provider,\n model: result.model,\n connectionId: null\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"connection_test_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAgentIdentities(auth.uid);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerAgentIdentity({\n ownerUid: auth.uid,\n agentId: payload?.agentId,\n role: payload?.role,\n personaSeed: payload?.personaSeed || {},\n capabilityLevel: payload?.capabilityLevel\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/agents/identities/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const owns = await getAgentIdentity(id);\n if (!owns || owns.ownerUid !== auth.uid) return sendJson(res, 403, { ok: false, error: \"forbidden\" });\n const payload = await readHttpJson(req);\n const row = await updateAgentIdentity(id, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/academy/events\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/academy/events\"}`);\n const status = String(url.searchParams.get(\"status\") || \"\");\n const items = await listAcademyEvents({ ownerUid: auth.uid, status, limit: 120 });\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/academy/events\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const ev = await queueAcademyEvent({\n type: payload?.type,\n roomId: payload?.roomId,\n ownerUid: auth.uid,\n participants: payload?.participants || [],\n topic: payload?.topic || \"academy\",\n payload: payload?.payload || {}\n });\n sendJson(res, 200, { ok: true, event: ev });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && String(req.url || \"\").startsWith(\"/academy/events/\") && String(req.url || \"\").endsWith(\"/resolve\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const parts = String(req.url || \"\").split(\"/\").filter(Boolean);\n const eventId = parts[2] || \"\";\n try {\n const payload = await readHttpJson(req);\n const out = await resolveAcademyEvent(eventId, payload || {});\n sendJson(res, 200, { ok: true, event: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_resolve_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/memory\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/memory\"}`);\n const scope = url.searchParams.get(\"scope\") === \"agents\" ? \"agents\" : \"users\";\n const id = String(url.searchParams.get(\"id\") || (scope === \"agents\" ? \"\" : auth.uid));\n const kind = String(url.searchParams.get(\"kind\") || \"\");\n const limit = Number(url.searchParams.get(\"limit\") || 40);\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n sendJson(res, 200, { ok: true, scope, id, kind, items: await listMemories({ scope, id, kind, limit }) });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const profile = await getPersonaGoalMemory(auth.uid);\n sendJson(res, 200, { ok: true, profile });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const profile = await setPersonaGoalMemory({\n uid: auth.uid,\n personaPatch: payload?.personaPatch || {},\n goals: Array.isArray(payload?.goals) ? payload.goals : null,\n preferencesPatch: payload?.preferencesPatch || {}\n });\n sendJson(res, 200, { ok: true, profile });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"profile_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/compact\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const out = await autoCompactMemories({ scope, id });\n sendJson(res, 200, { ok: true, scope, id, ...out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_compact_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/context\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const context = await getMemoryContext({\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n query: String(payload?.query || \"\"),\n limit: Number(payload?.limit || 80)\n });\n sendJson(res, 200, { ok: true, context });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_context_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/place-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const placeName = String(payload?.placeName || \"\").trim();\n if (!placeName) return sendJson(res, 400, { ok: false, error: \"placeName_required\" });\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const persona = String(payload?.waypointPersona || payload?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(placeName)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Place: ${placeName}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Give a concise conversational briefing about this place for a walking-route companion.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-place-brief\", placeName, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `PLACE_BRIEF:${placeName} => ${String(summary.reply || \"\").slice(0, 900)}`,\n tags: [\"place\", \"brief\", \"event-layer\"],\n importance: 0.62,\n meta: { placeName }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"place-brief\",\n text: `${placeName} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: {\n placeName,\n directive: summary.directive,\n lat: hit?.lat ? Number(hit.lat) : null,\n lon: hit?.lon ? Number(hit.lon) : null\n }\n });\n sendJson(res, 200, { ok: true, place: hit || null, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"place_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/route-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const waypoints = Array.isArray(payload?.waypoints) ? payload.waypoints.slice(0, 24) : [];\n const out = [];\n for (const wp of waypoints) {\n const name = String(wp?.name || \"\").trim();\n if (!name) continue;\n const persona = String(wp?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(name)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Waypoint: ${name}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Generate a concise spoken briefing for route waypoint.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-route-brief\", waypoint: name, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n out.push({\n name,\n persona,\n place: hit || null,\n reply: summary.reply,\n directive: summary.directive\n });\n }\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `ROUTE_BRIEF: waypoints=${out.length} ${out.map((x) => `${x.name}:${x.persona}`).join(\" | \")}`.slice(0, 1300),\n tags: [\"route\", \"waypoint\", \"brief\", \"event-layer\"],\n importance: 0.74,\n meta: { count: out.length }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"route-brief\",\n text: out.map((x) => `${x.name}:${String(x.reply || \"\").slice(0, 140)}`).join(\" | \").slice(0, 1400),\n roomId: \"studio-main\",\n meta: {\n waypointCount: out.length,\n waypoints: out.map((x) => ({\n name: x.name,\n persona: x.persona,\n lat: x.place?.lat ? Number(x.place.lat) : null,\n lon: x.place?.lon ? Number(x.place.lon) : null\n }))\n }\n });\n sendJson(res, 200, { ok: true, items: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"route_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/pdf-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const title = String(payload?.title || \"document\");\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const providedText = String(payload?.text || \"\");\n const url = String(payload?.url || \"\");\n let content = providedText;\n if (!content && url) content = await fetchTextFromUrl(url);\n if (!content) return sendJson(res, 400, { ok: false, error: \"text_or_supported_url_required\" });\n const summary = await queryRhizohLlm(\n {\n message: `Summarize this document and provide key points + practical actions.\\nTitle:${title}\\nContent:\\n${content.slice(0, 12000)}`,\n context: { source: \"event-layer-pdf-brief\", title },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `DOC_BRIEF:${title} => ${String(summary.reply || \"\").slice(0, 1000)}`,\n tags: [\"document\", \"brief\", \"library\", \"event-layer\"],\n importance: 0.7,\n meta: { title, url: url || null }\n });\n await addTranscript(auth.uid, {\n source: \"library-brief\",\n eventType: \"pdf-brief\",\n text: `${title} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { title }\n });\n sendJson(res, 200, { ok: true, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/pdf/upload\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req, 8 * 1024 * 1024);\n const job = await enqueuePdfJob({\n uid: auth.uid,\n fileName: payload?.fileName || \"document.pdf\",\n bytesBase64: payload?.contentBase64\n });\n sendJson(res, 200, { ok: true, job });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_upload_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/session\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createPublishSession(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session\",\n text: `Publish session armed: ${row.protocol} ${row.target}`.slice(0, 500),\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: row.id, bridge: row.bridge }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/studio/publish/sessions\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listPublishSessions(auth.uid, 80);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/health-check\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const protocol = String(payload?.protocol || \"WHIP\");\n const target = String(payload?.target || \"\");\n const health = await checkPublishTargetHealth(protocol, target);\n if (payload?.sessionId) {\n await updatePublishSession(auth.uid, String(payload.sessionId), {\n status: health.ok ? \"healthy\" : \"degraded\",\n reconnectPolicy: {\n enabled: true,\n backoffSec: [2, 5, 10, 20],\n nextRetryInSec: health.reconnectInSec || 0,\n maxRetries: 8,\n reason: health.reason\n }\n });\n }\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-health\",\n text: `${protocol} ${target} => ${health.ok ? \"healthy\" : \"degraded\"} (${health.reason})`,\n roomId: \"studio-main\",\n meta: { protocol, target, reconnect: !!health.shouldReconnect }\n });\n sendJson(res, 200, { ok: true, health });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_health_check_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/studio/publish/sessions/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updatePublishSession(auth.uid, id, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session-update\",\n text: `Session ${id} -> ${row.status || \"updated\"}`,\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: id, status: row.status, ingestUrl: row.ingestUrl || \"\" }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/transcripts\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/transcripts\"}`);\n const limit = Math.max(1, Math.min(200, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await listTranscripts(auth.uid, limit);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/transcripts\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await addTranscript(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"transcript_write_failed\" });\n }\n return;\n }\n\n /** E1: broadcast intent → GreenRoom capability → transcript + memory artifact */\n if (req.method === \"POST\" && req.url === \"/studio/capabilities/greenroom\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const traceId = String(payload?.traceId || `TRC-${Date.now().toString(36).toUpperCase()}`);\n const title = String(payload?.title || \"Castle Live Broadcast\").slice(0, 200);\n const intentRaw = String(payload?.intentRaw || \"\").slice(0, 800);\n const audienceEstimate = Math.max(0, Math.min(50_000_000, Number(payload?.audienceEstimate) || 1200));\n const roomId = String(payload?.roomId || \"greenroom-main\");\n const transcriptRow = await addTranscript(auth.uid, {\n source: \"rhizoh\",\n eventType: \"BROADCAST_ROUTED\",\n text: `GreenRoom · ${title}${intentRaw ? `\\nIntent: ${intentRaw}` : \"\"}`,\n roomId,\n meta: {\n ack: \"BROADCAST_ROUTED\",\n capability: \"greenroom\",\n intentType: \"broadcast\",\n traceId,\n audienceEstimate,\n artifact: {\n kind: \"broadcast_card\",\n title,\n traceId,\n roomId,\n createdAt: Date.now()\n }\n }\n });\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n text: `GREENROOM_BROADCAST:${title} trace=${traceId}`,\n tags: [\"broadcast\", \"greenroom\", \"artifact\", \"castle-library\"],\n importance: 0.72,\n kind: \"episodic\",\n meta: { traceId, capability: \"greenroom\", transcriptId: transcriptRow.id }\n });\n sendJson(res, 200, {\n ok: true,\n ack: \"BROADCAST_ROUTED\",\n traceId,\n transcript: transcriptRow,\n replayPath: `/replay/${encodeURIComponent(traceId)}`,\n sharePath: `/replay/${encodeURIComponent(traceId)}`\n });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"greenroom_capability_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/metadata/search\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/metadata/search\"}`);\n const q = String(url.searchParams.get(\"q\") || \"\");\n const limit = Math.max(1, Math.min(300, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await searchMetadata(auth.uid, q, limit);\n sendJson(res, 200, { ok: true, q, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerDevice(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, device: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"device_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const lat = Number(payload?.lat);\n const lon = Number(payload?.lon);\n const speed = Math.max(0, Number(payload?.speed || 0));\n const cfgCenterLat = Number(process.env.CASTLE_GEOFENCE_CENTER_LAT || 41.0082);\n const cfgCenterLon = Number(process.env.CASTLE_GEOFENCE_CENTER_LON || 28.9784);\n const cfgRadiusM = Math.max(100, Number(process.env.CASTLE_GEOFENCE_RADIUS_M || 55000));\n const cfgSpeedCap = Math.max(1, Number(process.env.CASTLE_SPEED_CAP_MPS || 35));\n const dist = Number.isFinite(lat) && Number.isFinite(lon) ? haversineMeters(cfgCenterLat, cfgCenterLon, lat, lon) : 0;\n const geofenceOk = !Number.isFinite(dist) ? true : dist <= cfgRadiusM;\n const speedOk = speed <= cfgSpeedCap;\n const failsafe = !(geofenceOk && speedOk);\n const safety = {\n geofenceOk,\n speedOk,\n failsafe,\n distanceFromCenterM: Number.isFinite(dist) ? dist : null,\n speedCapMps: cfgSpeedCap\n };\n const row = await appendTelemetry(auth.uid, deviceId, {\n lat,\n lon,\n speed,\n battery: payload?.battery,\n mode: payload?.mode || (failsafe ? \"FAILSAFE_HOLD\" : \"ACTIVE\"),\n raw: payload?.raw || {}\n });\n await updateDevice(auth.uid, deviceId, {\n status: failsafe ? \"failsafe\" : \"active\",\n lastTelemetryAt: row.ts,\n safetyState: safety\n });\n if (failsafe) {\n await addTranscript(auth.uid, {\n source: \"robotics-guardrails\",\n eventType: \"failsafe\",\n text: `Device ${deviceId} failsafe triggered geofenceOk=${geofenceOk} speedOk=${speedOk}`,\n roomId: \"studio-main\",\n meta: { deviceId, safety }\n });\n }\n sendJson(res, 200, { ok: true, telemetry: row, safety, command: failsafe ? \"FAILSAFE_HOLD\" : \"CONTINUE\" });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"telemetry_ingest_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listTelemetry(auth.uid, 150);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createAutomation(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"automation-scheduled\",\n text: `Automation scheduled: ${row.title} at ${new Date(row.scheduleAt).toLocaleString()}`,\n roomId: \"studio-main\",\n meta: { automationId: row.id, action: row.action, deviceId: row.deviceId }\n });\n sendJson(res, 200, { ok: true, automation: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"automation_create_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAutomations(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listDevices(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const devs = await listDevices(auth.uid, 500);\n const d = devs.find((x) => x.id === deviceId);\n if (!d) return sendJson(res, 404, { ok: false, error: \"device_not_found\" });\n const action = String(payload?.action || \"\");\n const caps = Array.isArray(d.capabilityProfile) ? d.capabilityProfile : [];\n if (action && caps.length && !caps.includes(action) && !caps.includes(\"*\")) {\n return sendJson(res, 400, { ok: false, error: \"unsupported_action_for_device\", capabilityProfile: caps });\n }\n const cmd = await enqueueCommand(auth.uid, {\n deviceId,\n action: action || \"NOOP\",\n params: payload?.params || {},\n adapter: payload?.adapter || d.adapter || \"websocket\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-command-queue\",\n eventType: \"command-queued\",\n text: `${cmd.deviceId} -> ${cmd.action}`,\n roomId: \"studio-main\",\n meta: { commandId: cmd.id, adapter: cmd.adapter }\n });\n sendJson(res, 200, { ok: true, command: cmd });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listCommands(auth.uid, 200);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/robotics/commands/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updateCommandStatus(auth.uid, id, payload?.status || \"sent\");\n sendJson(res, 200, { ok: true, command: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/rhizoh/bridge\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const result = await runRhizohBrainV2({\n message: String(payload?.message || \"robotics bridge command\"),\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n worldState: payload?.worldState || {},\n deviceState: payload?.deviceState || {},\n intent: \"robotics-mechanics\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-bridge\",\n eventType: \"robotics-command\",\n text: `Bridge -> ${String(result.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { directive: result.directive, agentId: payload?.agentId || \"\" }\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"robotics_bridge_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/planner/graph\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const graph = buildTaskActionGraph(String(payload?.task || \"\"), payload?.context || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"planner-graph\",\n text: `Planner graph built: ${graph.taskTitle} nodes=${graph.nodes.length}`,\n roomId: \"studio-main\",\n meta: { taskTitle: graph.taskTitle, nodeCount: graph.nodes.length }\n });\n sendJson(res, 200, { ok: true, graph });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"planner_graph_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerSocialChannel(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, channel: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_channel_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listSocialChannels(auth.uid, 100);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/broadcast\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const text = String(payload?.text || \"\").slice(0, 1200);\n const channels = await listSocialChannels(auth.uid, 100);\n const enabled = channels.filter((c) => c.status === \"enabled\");\n const deliveries = [];\n for (const c of enabled) {\n const out = await deliverSocialWithRetry(c, text);\n deliveries.push({ id: c.id, platform: c.platform, ...out });\n }\n const okCount = deliveries.filter((d) => d.ok).length;\n await addTranscript(auth.uid, {\n source: \"social-layer\",\n eventType: \"social-broadcast\",\n text: `Broadcast delivered ${okCount}/${enabled.length} channel(s): ${text.slice(0, 240)}`,\n roomId: \"studio-main\",\n meta: { deliveries }\n });\n sendJson(res, 200, { ok: true, sentChannels: okCount, totalChannels: enabled.length, deliveries });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_broadcast_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/ethics/programs\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, ...buildEthicsPrograms() });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands/dispatch\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const all = await listCommands(auth.uid, 200);\n const queued = all.filter((c) => String(c.status || \"queued\") === \"queued\").slice(0, 50);\n const devices = await listDevices(auth.uid, 500);\n const byId = new Map(devices.map((d) => [d.id, d]));\n const results = [];\n for (const cmd of queued) {\n const dev = byId.get(cmd.deviceId);\n if (!dev) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: \"device_not_found\" });\n continue;\n }\n try {\n const dispatched = await dispatchCommandToAdapter(dev, cmd);\n await updateCommandStatus(auth.uid, cmd.id, \"sent\");\n results.push({ id: cmd.id, ok: true, adapter: dispatched.adapter, via: dispatched.via });\n } catch (error) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: String(error?.message || \"dispatch_failed\") });\n }\n }\n await addTranscript(auth.uid, {\n source: \"robotics-command-dispatch\",\n eventType: \"command-dispatch-batch\",\n text: `Dispatch batch size=${results.length} ok=${results.filter((x) => x.ok).length}`,\n roomId: \"studio-main\",\n meta: { results: results.slice(0, 30) }\n });\n sendJson(res, 200, { ok: true, processed: results.length, results });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"dispatch_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/pdf/jobs/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n const job = await getPdfJob(auth.uid, id);\n if (!job) return sendJson(res, 404, { ok: false, error: \"pdf_job_not_found\" });\n sendJson(res, 200, { ok: true, job });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const row = await appendMemory({\n scope,\n id,\n text: payload?.text,\n tags: payload?.tags || [],\n importance: payload?.importance,\n kind: payload?.kind || \"episodic\",\n meta: { uid: auth.uid, ...(payload?.meta || {}) }\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.externalTruth) {\n try {\n if (process.env.RHIZOH_EXTERNAL_TRUTH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_truth:${ip}`, RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n sendJson(res, 200, buildRhizohExternalGroundTruthPayload(req));\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.productOutcome) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const rawBody = await readHttpBodyText(req, 64 * 1024);\n const body = rawBody.trim() ? safeJsonParse(rawBody) : null;\n if (!body || typeof body !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"invalid_json_payload\" });\n return;\n }\n const sigHdr = String(req.headers?.[\"x-rhizoh-outcome-signature\"] || \"\").trim();\n const hmacV = verifyRhizohOutcomeHmac(rawBody, sigHdr);\n if (!hmacV.ok) {\n sendJson(res, 401, { ok: false, error: hmacV.error || \"outcome_hmac_invalid\" });\n return;\n }\n const srcTokHdr = String(req.headers?.[\"x-rhizoh-outcome-source-token\"] || \"\").trim();\n const srcV = verifyRhizohOutcomeSourceToken(body.source, srcTokHdr);\n if (!srcV.ok) {\n sendJson(res, 401, { ok: false, error: srcV.error || \"outcome_source_token_invalid\" });\n return;\n }\n const subjectKey = String(body.subjectKey || \"\").trim().slice(0, 128);\n if (\n subjectKey &&\n !checkHttpRateLimit(\n `rhizoh_product_outcome_subj:${subjectKey}`,\n RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN,\n 60_000\n )\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_subject\" });\n return;\n }\n const result = ingestRhizohProductOutcomeHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n schemaVersion: \"1.1.0\",\n aggregate: result.aggregate,\n integrity: result.integrity\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"outcome_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.productOutcomeAggregate) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome_agg:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const u = new URL(String(req.url || \"/\"), \"http://localhost\");\n const cohortId = u.searchParams.get(\"cohortId\") || \"\";\n const decisionFingerprint = u.searchParams.get(\"decisionFingerprint\") || \"\";\n const limit = u.searchParams.get(\"limit\") || \"50\";\n const rows = listRhizohProductOutcomeAggregates({\n cohortId: cohortId || undefined,\n decisionFingerprint: decisionFingerprint || undefined,\n limit: Number(limit) || 50\n });\n sendJson(res, 200, { ok: true, schemaVersion: \"1.1.0\", rows });\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.externalLossBatch) {\n try {\n if (process.env.RHIZOH_EXTERNAL_LOSS_BATCH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_loss_batch:${ip}`, RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const result = ingestRhizohExternalLossBatchHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, { ok: true, accepted: result.accepted ?? 0 });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"batch_failed\") });\n }\n return;\n }\n\n function moderationAdminOk(req) {\n const expected = String(process.env.CASTLE_MODERATION_ADMIN_KEY || \"\").trim();\n if (!expected) return false;\n const got = String(req.headers[\"x-castle-moderation-key\"] || \"\").trim();\n return got.length > 0 && got === expected;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.opsAbuseReport) {\n try {\n const body = await readHttpJson(req, 32 * 1024);\n const auth = await resolveHttpUser(req);\n const entry = submitAbuseReportV0({\n reporterUid: auth.ok ? auth.uid : null,\n targetUid: body?.targetUid ?? null,\n category: body?.category,\n detail: body?.detail,\n traceId: body?.traceId ?? null\n });\n sendJson(res, 202, { ok: true, reportId: entry.id, status: entry.status });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsModerationQueue) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n queue: listModerationQueueV0(100),\n softBlocked: listSoftBlockedUsersV0()\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-block\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const out = softBlockUserV0(body?.uid, body?.reason);\n sendJson(res, out.ok ? 200 : 400, out);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-unblock\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n sendJson(res, 200, removeSoftBlockV0(body?.uid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsAgentSnapshots) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n const limit = Number(new URL(req.url || \"\", \"http://local\").searchParams.get(\"limit\") || 50);\n sendJson(res, 200, { ok: true, snapshots: listRecentAgentSnapshotsV0(limit) });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.academicObservatoryExport) {\n const auth = await resolveHttpUser(req);\n const out = handleAcademicObservatoryExportGetV0(req, auth);\n sendJson(res, out.status, out.body);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsHardeningStatus) {\n const auth = await resolveHttpUser(req);\n const principal = auth.ok ? `uid:${auth.uid}` : null;\n const gclSnap = await getGlobalCostLedgerSnapshotV0(principal);\n const economicStrategy = evaluateEconomicStrategyL3V0(gclSnap);\n const hardeningTenantId = resolveHardeningTenantIdV0(auth, req);\n const unifiedState = await buildUnifiedStateNarrativeV0({\n tenantId: hardeningTenantId ?? undefined,\n platformScope: !hardeningTenantId,\n principal\n });\n sendJson(res, 200, {\n ok: true,\n schema: \"rhizoh.ops.hardening_status.v0\",\n containment: readAgentContainmentConfigV0(),\n cost: readCostContainmentConfigV0(),\n gcl: {\n backend: await getGclBackendStatusV0(),\n snapshot: gclSnap\n },\n economicStrategyL3: economicStrategy,\n phasedRollout: await getPhasedRolloutStatsV0(),\n tier: readPhasedRolloutTierV0(),\n principalCost: auth.ok ? await getCostContainmentStatsV0(principal) : null,\n stressTaxonomy: verifyStressTaxonomyCoverageV0(),\n stressConflictResolution: verifyStressConflictResolutionV0(),\n resolutionStabilityEnvelope: verifyResolutionStabilityEnvelopeV0(),\n unifiedState: {\n tenantScope: {\n tenantId: unifiedState.tenantScope?.tenantId,\n scope: unifiedState.tenantScope?.scope,\n noGlobalDerivedState: unifiedState.tenantScope?.noGlobalDerivedState\n },\n narrativeFingerprint: unifiedState.narrativeFingerprint,\n screenshotScopeWatermark: unifiedState.screenshotScopeWatermark,\n interpretationSafetyContract: unifiedState.interpretationSafetyContract,\n interpretationUxContract: {\n schema: unifiedState.interpretationUxContract?.schema,\n layerOrder: unifiedState.interpretationUxContract?.layerOrder,\n narrativeNeverVisuallyDominant: unifiedState.interpretationUxContract?.narrativeNeverVisuallyDominant\n },\n governance: {\n narrativeIsNotDecisionLayer: unifiedState.governance.narrativeIsNotDecisionLayer,\n displayOrder: unifiedState.governance.displayOrder,\n trustPosture: unifiedState.governance.trustPosture,\n interpretationUxContract: unifiedState.governance.interpretationUxContract?.schema\n },\n humanOps: {\n decisionTier: unifiedState.humanOps?.humanDecisionScaling?.tier,\n decisionOwner: unifiedState.humanOps?.humanDecisionScaling?.decisionOwner,\n opsRouteId: unifiedState.humanOps?.humanDecisionOpsRunbook?.routing?.routeId,\n slaAckMinutes: unifiedState.humanOps?.humanDecisionOpsRunbook?.sla?.ackMinutes,\n uiDisclaimerRequired: unifiedState.humanOps?.narrativeUiSafety?.displayRules?.requireDisclaimerBanner,\n misreadHighResidual: unifiedState.humanOps?.misreadSimulation?.highResidualCount,\n propagationHighResidual: unifiedState.humanOps?.socialPropagationSimulation?.highResidualCount,\n worstWatermarkSurvivability:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.worstWatermarkSurvivability,\n dominantDistortionSource:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.dominantDistortionSource\n },\n culturalRisk: {\n perceptualRiskClass: unifiedState.culturalRisk?.perceptualRiskClass,\n trustFork: unifiedState.culturalRisk?.trustDynamics?.fork,\n trustDecayScore: unifiedState.culturalRisk?.trustDynamics?.scores?.trustDecay,\n mythologyScore: unifiedState.culturalRisk?.trustDynamics?.scores?.mythology,\n confidenceDetachmentExposure:\n unifiedState.culturalRisk?.trustDynamics?.axes?.confidenceDetachmentExposure\n },\n appliedSystemsLayer: {\n schema: unifiedState.appliedSystemsLayer?.schema,\n researchOutputType: unifiedState.appliedSystemsLayer?.researchGovernance?.outputType,\n roboticsBlockActuation: unifiedState.appliedSystemsLayer?.roboticsGrounding?.blockActuation,\n spiralRumorCount:\n unifiedState.appliedSystemsLayer?.spiralMMOGameKernel?.gameLayer?.rumors?.length\n },\n actionSemanticGovernance: {\n schema: unifiedState.actionSemanticGovernance?.schema,\n highRiskActionCount: unifiedState.actionSemanticGovernance?.highRiskActionCount,\n loopClosed: unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.loopClosed,\n authorityCollapseMitigated:\n unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.risks?.authorityCollapse\n ?.mitigated\n },\n actionContextResolution: {\n schema: unifiedState.actionContextResolution?.schema,\n contextFingerprint: unifiedState.actionContextResolution?.contextFingerprint?.fingerprintHash,\n domainCollision: unifiedState.actionContextResolution?.domainCollision?.detected,\n deployAgentSpiralBlocked:\n unifiedState.actionContextResolution?.actionOverloadingRisk?.deployAgentExample\n ?.spiral_mmo === \"blocked\",\n roboticsActuationEligible:\n unifiedState.actionContextResolution?.roboticsActuationCorrectness?.actuationEligible\n },\n epistemicCoherence: {\n schema: unifiedState.epistemicCoherence?.schema,\n verdict: unifiedState.epistemicCoherence?.systemCoherence?.verdict,\n coherenceScore: unifiedState.epistemicCoherence?.systemCoherence?.score,\n contradictionCount: unifiedState.epistemicCoherence?.crossLayerContradictions?.count,\n operatorDecision: unifiedState.epistemicCoherence?.uxCompression?.operatorDecision,\n headlineTr: unifiedState.epistemicCoherence?.uxCompression?.headline?.tr,\n falseSafetyWarning: Boolean(\n unifiedState.epistemicCoherence?.uxCompression?.falseSafetyWarning\n )\n },\n coherenceAuthorityBoundary: {\n schema: unifiedState.coherenceAuthorityBoundary?.schema,\n bindingValid: unifiedState.coherenceAuthorityBoundary?.eclAuthorityBinding?.valid,\n canExecute: unifiedState.coherenceAuthorityBoundary?.contract?.can_execute,\n mandatoryBannerTr:\n unifiedState.coherenceAuthorityBoundary?.disclaimers?.mandatoryBanner?.tr\n },\n decisionLatencyGovernance: {\n schema: unifiedState.decisionLatencyGovernance?.schema,\n latencyTier: unifiedState.decisionLatencyGovernance?.latencyTier,\n inflationScore: unifiedState.decisionLatencyGovernance?.latencyInflation?.score,\n fastPathEligible: unifiedState.decisionLatencyGovernance?.fastPath?.eligible,\n primaryActionId: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.primaryActionId,\n ackSlaMinutes: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.ackSlaMinutes,\n certaintyCap: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.certaintyCap,\n uncertaintyItems:\n unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.uncertaintyEnvelope?.itemCount\n },\n decisionPacketUncertaintyBoundary: {\n schema: unifiedState.decisionPacketUncertaintyBoundary?.schema,\n invariantValid: unifiedState.decisionPacketUncertaintyBoundary?.invariantCheck?.valid,\n centralInvariant: unifiedState.decisionPacketUncertaintyBoundary?.centralInvariant\n },\n epistemicDecisionPacing: {\n schema: unifiedState.epistemicDecisionPacing?.schema,\n queueSaturationTier:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation?.tier,\n operatorProcessingLatency:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation\n ?.operatorProcessingLatency,\n postureWindowCount:\n unifiedState.epistemicDecisionPacing?.temporaryStaticPostureWindows\n ?.temporaryStaticPostureWindows?.windowCount,\n executionBoundariesValid:\n unifiedState.epistemicDecisionPacing?.executionBoundaries?.valid,\n grantsExecutionApproval: unifiedState.epistemicDecisionPacing?.grantsExecutionApproval\n },\n epistemicTemporalCoherence: {\n schema: unifiedState.epistemicTemporalCoherence?.schema,\n allInvariantsValid: unifiedState.epistemicTemporalCoherence?.allInvariantsValid,\n temporalAlignmentValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.temporalAlignment?.valid,\n crossWindowGuardValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard?.valid,\n delayedTruthValid: unifiedState.epistemicTemporalCoherence?.invariants?.delayedTruthRule?.valid,\n reconciliationRequired:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard\n ?.reconciliation?.required\n },\n realityDriftObserver: {\n schema: unifiedState.realityDriftObserver?.schema,\n driftActive: unifiedState.realityDriftObserver?.driftObserverSummary?.active,\n misapprehensionShapeCount:\n unifiedState.realityDriftObserver?.misapprehensionShapeCatalog?.shapeCount,\n criticalShapeCount:\n unifiedState.realityDriftObserver?.driftObserverSummary?.criticalShapeCount,\n roboticsMismatchActive:\n unifiedState.realityDriftObserver?.roboticsFeedbackMismatch?.active\n },\n driftCausality: {\n schema: unifiedState.driftCausality?.schema,\n shapesWithCausalExplanation: unifiedState.driftCausality?.rdolLinkage?.shapesWithCausalExplanation,\n activeDomainCount: unifiedState.driftCausality?.causalitySummary?.activeDomainCount,\n propagationCausalityActive: unifiedState.driftCausality?.causalDomains?.propagation?.active\n },\n productReadiness: unifiedState.productReadiness,\n stateLayers: {\n raw: unifiedState.stateLayers.raw,\n derived: {\n systemState: unifiedState.stateLayers.derived.systemState,\n narrative: unifiedState.stateLayers.derived.narrative,\n validation: unifiedState.stateLayers.derived.validation\n },\n policy: {\n binding: unifiedState.stateLayers.policy.binding,\n decisionOwner: unifiedState.stateLayers.policy.decisionOwner,\n prohibitedActions: unifiedState.stateLayers.policy.prohibitedActions\n }\n },\n systemState: unifiedState.systemState,\n interpretation: unifiedState.interpretation,\n validation: {\n trustPosture: unifiedState.validation.trustPosture,\n adjustedConfidence: unifiedState.validation.adjustedConfidence,\n agreement: unifiedState.validation.confidenceDecomposition.agreement,\n divergenceScore: unifiedState.validation.divergence.divergenceScore,\n validated: unifiedState.validation.divergence.validated,\n uncertaintyTags: unifiedState.validation.uncertainty.tags.map((t) => t.id),\n flags: unifiedState.validation.divergence.flags.map((f) => f.id)\n },\n compression: unifiedState.compression\n }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 6 * 1024 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (\n !checkHttpRateLimit(`rhizoh_voice_transcribe:${rlKey}`, RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN, 60_000)\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const voiceEnv = rhizohVoiceTranscribeEnvV3();\n if (!voiceEnv.any) {\n sendJson(res, 503, {\n ok: false,\n error: \"voice_asr_not_configured\",\n env: voiceEnv\n });\n return;\n }\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n const result = await runRhizohVoiceTranscribeV3(\n applyLanguagePropagationToVoiceBodyV1(payload, langProp)\n );\n sendJson(res, result.ok ? 200 : 422, {\n ...result,\n traceId: String(payload?.traceId || \"\"),\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n atMs: Date.now()\n });\n } catch (error) {\n sendJson(res, 500, {\n ok: false,\n error: \"voice_transcribe_failed\",\n detail: String(error?.message || error),\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(\"/rhizoh/llm/task/\")) {\n const rawTaskId = pathname.slice(\"/rhizoh/llm/task/\".length).split(\"/\")[0];\n const taskId = String(rawTaskId || \"\").toLowerCase() === \"last\" ? getLatestLlmWorkerTaskIdV0() : rawTaskId;\n const snap = taskId ? getLlmWorkerTaskSnapshotV0(taskId) : null;\n if (!snap) {\n sendJson(res, 404, {\n ok: false,\n error: \"llm_task_not_found\",\n taskId: rawTaskId,\n status: \"missing\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n if (snap.status === \"processing\") {\n sendJson(res, 202, {\n ok: true,\n taskId: snap.taskId,\n status: \"processing\",\n createdAt: snap.createdAt,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n sendJson(res, snap.httpStatus || 200, {\n ...(snap.body || { ok: false, error: \"llm_task_empty\", taskId: snap.taskId }),\n taskId: snap.taskId,\n status: snap.status === \"completed\" ? \"completed\" : snap.status,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n\n if (req.method === \"POST\" && req.url === rhizohRuntime.routes.rhizohLlm) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 128 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n logRhizohHealth(\"gateway_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: String(payload?.traceId || \"\"),\n auth: auth.ok ? \"ok\" : \"anon\",\n ip\n });\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (!checkHttpRateLimit(`rhizoh_llm:${rlKey}`, RL_RHIZOH_LLM_PER_MIN, 60_000)) {\n const rlTaxonomy = classifyStressResponseV0({ code: \"rate_limit_exceeded\" });\n return sendJson(res, 429, {\n ok: false,\n error: \"rate_limit_exceeded\",\n reply: rlTaxonomy.userMessageTr || \"İstek sınırı aşıldı. Kısa süre sonra tekrar deneyin.\",\n stressClass: rlTaxonomy.stressClass,\n responseAction: rlTaxonomy.responseAction,\n stressMatrix: rlTaxonomy.matrix,\n stressInterpretable: rlTaxonomy.interpretable,\n stressConfidence: rlTaxonomy.stressConfidence,\n actionConfidence: rlTaxonomy.actionConfidence,\n actionSoftened: rlTaxonomy.actionSoftened,\n responseActionStrict: rlTaxonomy.responseActionStrict,\n actionInterpretable: rlTaxonomy.actionInterpretable,\n conflictResolution: rlTaxonomy.conflictResolution,\n directive: \"NONE\"\n });\n }\n\n const requireExplicit = process.env.CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE === \"1\";\n const keyMode = normalizeClientLlmKeySource(payload?.llmKeySource ?? payload?.keySource);\n if (keyMode === null) {\n return sendJson(res, 400, {\n ok: false,\n error: \"invalid_llm_key_source\",\n reply: \"llmKeySource geçersiz. Kullanın: env | user_connection | auto\",\n directive: \"NONE\"\n });\n }\n if (requireExplicit && keyMode === \"auto\") {\n return sendJson(res, 400, {\n ok: false,\n error: \"llm_key_source_required\",\n reply: \"Sunucu CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE=1: istekte llmKeySource (env veya user_connection) zorunlu.\",\n directive: \"NONE\"\n });\n }\n\n const conn = auth.ok ? await resolveConnection(auth.uid, String(payload?.connectionId || \"\")) : null;\n\n if (keyMode === \"user_connection\") {\n if (!auth.ok) {\n return sendJson(res, 401, {\n ok: false,\n error: \"llm_auth_required_for_user_connection\",\n reply: \"Kayıtlı LLM bağlantısı için giriş gerekli.\",\n directive: \"NONE\"\n });\n }\n if (!conn?.apiKey) {\n return sendJson(res, 400, {\n ok: false,\n error: \"user_llm_connection_required\",\n reply: \"Geçerli bir LLM bağlantısı yok; Studio’da bağlantı ekleyin veya llmKeySource=env kullanın.\",\n directive: \"NONE\"\n });\n }\n }\n\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n let safePayload = applyLanguagePropagationToLlmPayloadV1({ ...(payload || {}) }, langProp);\n const trimOut = trimRhizohLlmGatewayPayloadV1(safePayload);\n if (trimOut.meta.trimmed) {\n logRhizohHealth(\"llm_payload_trimmed\", {\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n bytesBefore: trimOut.meta.bytesBefore,\n bytesAfter: trimOut.meta.bytesAfter\n });\n }\n safePayload = trimOut.payload;\n delete safePayload.apiKey;\n delete safePayload.llmKeySource;\n delete safePayload.keySource;\n\n const llmMessage = String(\n safePayload?.message || safePayload?.text || payload?.message || payload?.text || \"\"\n ).trim();\n if (!llmMessage) {\n return sendJson(res, 400, {\n ok: false,\n error: \"message_required\",\n reply: \"Mesaj metni gerekli (message alanı boş).\",\n directive: \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n if (!safePayload.message) {\n safePayload = { ...safePayload, message: llmMessage };\n }\n\n let resolvedProvider = payload?.provider;\n let resolvedModel = payload?.model;\n let connApiKey = \"\";\n\n if (keyMode === \"user_connection\") {\n resolvedProvider = conn.provider;\n resolvedModel = conn.model;\n connApiKey = conn.apiKey || \"\";\n } else if (keyMode === \"env\") {\n connApiKey = \"\";\n } else {\n resolvedProvider = conn?.provider || payload?.provider;\n resolvedModel = conn?.model || payload?.model;\n connApiKey = conn?.apiKey || \"\";\n }\n\n const turnInput = {\n safePayload,\n auth,\n keyMode,\n conn,\n resolvedProvider,\n resolvedModel,\n connApiKey\n };\n const turnMeta = {\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n langEcho: buildRhizohLanguagePropagationEchoV1(langProp)\n };\n\n if (isLlmWorkerEnabledV0() && isLlmWorkerAsyncEnabledV0()) {\n const taskId = enqueueLlmWorkerTaskAsyncV0(turnInput, turnMeta);\n logRhizohHealth(\"llm_worker_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n taskId,\n traceId: String(payload?.traceId || taskId),\n keyMode,\n async: true\n });\n sendJson(res, 202, {\n ok: true,\n taskId,\n status: \"processing\",\n traceId: String(payload?.traceId || taskId),\n pollPath: `/rhizoh/llm/task/${taskId}`,\n ...turnMeta.langEcho\n });\n return;\n }\n\n const { result, traceId, turnLatencyMs, spinePhases, sampledTrace } = isLlmWorkerEnabledV0()\n ? await runLlmWorkerTaskV0(turnInput, turnMeta)\n : await rhizohGatewayTurn(turnInput);\n logRhizohHealth(\"llm_response\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId,\n turnLatencyMs: Number(turnLatencyMs || 0),\n keyMode\n });\n sendJson(res, 200, {\n ...result,\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n traceId,\n turnLatencyMs,\n ...(spinePhases ? { spinePhases } : {}),\n ...(sampledTrace !== undefined ? { sampledTrace } : {})\n });\n } catch (error) {\n const msg = String(error?.message || \"\");\n const code = error?.code || \"\";\n let status = 500;\n if (msg === \"payload_too_large\") status = 413;\n else if (msg === \"rate_limit_exceeded\") status = 429;\n else if (\n code === \"cost_hard_limit\" ||\n code === \"phased_rollout_capacity\" ||\n error?.containment === true ||\n String(code).startsWith(\"agent_\")\n ) {\n status = 429;\n } else if (code === \"user_soft_blocked\" || code === \"prompt_abuse_detected\") status = 403;\n else if (\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(code) ||\n [\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(msg) ||\n msg.includes(\"missing_api_key_for_\")\n )\n status = 400;\n let rhizohFailureKind = \"provider_error\";\n if (msg === \"rate_limit_exceeded\" || status === 429) rhizohFailureKind = \"rate_limit\";\n else if (status === 403) rhizohFailureKind = \"policy_block\";\n else if (status === 400 || status === 413) rhizohFailureKind = \"client_config\";\n else if (msg.startsWith(\"provider_http_\")) {\n const st = Number(msg.replace(\"provider_http_\", \"\"));\n if (st === 408 || st === 504) rhizohFailureKind = \"timeout\";\n else rhizohFailureKind = \"provider_error\";\n } else if (/timeout|timed out|ETIMEDOUT/i.test(msg)) rhizohFailureKind = \"timeout\";\n\n const providerHttpMatch = msg.match(/^provider_http_(\\d{3})$/);\n const stressTaxonomy =\n error?.stressClass != null\n ? {\n stressClass: error.stressClass,\n responseAction: error.responseAction,\n stressMatrix: error.stressMatrix,\n stressInterpretable: error.stressInterpretable !== false,\n stressConfidence: error.stressConfidence,\n actionConfidence: error.actionConfidence,\n actionSoftened: error.actionSoftened,\n responseActionStrict: error.responseActionStrict,\n actionInterpretable: error.actionInterpretable,\n conflictResolution: error.conflictResolution,\n stressSecondary: error.stressSecondary,\n responseActions: error.responseActions\n }\n : (() => {\n const t = classifyStressResponseV0({\n code: code || msg,\n rhizohFailureKind,\n providerHttpStatus: providerHttpMatch\n ? Number(providerHttpMatch[1])\n : null\n });\n return {\n stressClass: t.stressClass,\n responseAction: t.responseAction,\n stressMatrix: t.matrix,\n stressInterpretable: t.interpretable,\n stressConfidence: t.stressConfidence,\n actionConfidence: t.actionConfidence,\n actionSoftened: t.actionSoftened,\n responseActionStrict: t.responseActionStrict,\n actionInterpretable: t.actionInterpretable,\n conflictResolution: t.conflictResolution,\n stressSecondary: t.stressSecondary,\n responseActions: t.responseActions\n };\n })();\n sendJson(res, status, {\n ok: false,\n rhizohFailureKind,\n ...(providerHttpMatch ? { providerHttpStatus: Number(providerHttpMatch[1]) } : {}),\n error: code || msg || \"rhizoh_llm_failed\",\n detail: msg || code || undefined,\n reply:\n error?.reply ||\n (status === 500 ? \"Rhizoh bağlantısı geçici olarak kesildi.\" : msg),\n directive: error?.directive || \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n ...stressTaxonomy\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicSeal) {\n try {\n if (!EPISTEMIC_SEAL_SECRET) {\n sendJson(res, 503, { ok: false, error: \"epistemic_seal_disabled\" });\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`epistemic_seal:${ip}`, RL_EPISTEMIC_SEAL_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const truth = body.truth_contract;\n if (!truth || typeof truth !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"truth_contract_required\" });\n return;\n }\n const sealBody = {\n truth_contract: truth,\n runtime_hash: String(body.runtime_hash || \"\"),\n model_route: {\n provider: body.model_route?.provider ?? null,\n model: body.model_route?.model ?? null\n },\n memory_digest: String(body.memory_digest || \"\"),\n world_snapshot_hash: String(body.world_snapshot_hash || \"\"),\n timestamp: Number(body.timestamp) || Date.now()\n };\n const canonical = canonicalEpistemicSealString(sealBody);\n const { hash, signature } = hashAndSignEpistemicSeal(canonical, EPISTEMIC_SEAL_SECRET);\n recordGenesisEpistemicSealIssued(hash);\n const attestation = {\n schema: EPISTEMIC_SEAL_SCHEMA,\n algorithm: \"SHA-256+HMAC-SHA256\",\n keyHint: process.env.CASTLE_EPISTEMIC_SEAL_SECRET ? \"CASTLE_EPISTEMIC_SEAL_SECRET\" : \"CASTLE_GATEWAY_TOKEN\",\n issuedAt: Date.now(),\n gateway: { service: \"castle-gateway\", listenPort: PORT }\n };\n sendJson(res, 200, {\n ok: true,\n hash,\n signature,\n attestation,\n canonicalBytes: Buffer.byteLength(canonical, \"utf8\")\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"seal_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicLogsBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`epistemic_logs:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 120) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const persisted = await persistEpistemicLedgerBatch(auth.uid, entries);\n recordGenesisEpistemicLedgerPersisted(persisted.persisted);\n // Internal cognition layer: forecast stays gateway/ledger-only (no client exposure).\n try {\n await persistEpistemicForecastBatch(auth.uid, persisted.normalized || []);\n } catch {\n /* forecast failures must not break primary ledger ingest */\n }\n sendJson(res, 200, {\n ok: true,\n persisted: persisted.persisted,\n mode: persisted.mode,\n latest: persisted.normalized?.[persisted.normalized.length - 1] || null\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"epistemic_log_ingest_failed\") });\n }\n return;\n }\n\n sendJson(res, 404, { ok: false, error: \"not_found\" });\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\nspiralWssForGenesis = wss;\nconst orchestrator = createOrchestrator();\nconst clientStats = new Map();\nlet broadcasterClientId = null;\nconst spiralState = {\n rooms: [{ id: \"room-default\", name: \"Spiral Default Arena\" }],\n activeByClient: new Map(),\n characters: new Map() // id -> {id,name,role,roomId,source}\n};\n\n/** Castle social rooms — keyed by `castleRoomKey` from {@link WS_MESSAGE.CASTLE_SOCIAL_PULSE}. */\nconst castleSocialRoomState = new Map();\nconst CASTLE_SOCIAL_MEMBER_TTL_MS = 90_000;\nconst CASTLE_SOCIAL_MAX_PULSE_BYTES = 8192;\nconst RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES = 5 * 1024 * 1024;\nconst RHIZOH_VOICE_LIVE_MAX_CHUNKS = 96;\n\n/**\n * Voice Engine v3 live media lane.\n * Client streams MediaRecorder chunks over the existing gateway WS; STOP reuses the\n * gateway ASR runner so orchestration remains the single authority downstream.\n */\nconst rhizohVoiceLiveSessions = new WeakMap();\n\nfunction appendRhizohVoiceLiveEventV0(session, type, payloadRefSeed = \"\") {\n if (!session) return null;\n session.eventSeq = (Number(session.eventSeq) || 0) + 1;\n const event = createRhizohImmutableEventV0({\n type,\n sessionId: session.sessionId,\n traceId: session.traceId,\n actorSeed: session.sessionId || session.traceId,\n eventSeq: session.eventSeq,\n payloadRef: createRhizohPayloadRefV0(payloadRefSeed || `${session.sessionId}:${type}:${session.eventSeq}`)\n });\n session.events = [...(session.events || []), event].slice(-64);\n return event;\n}\n\nfunction sendRhizohVoiceLiveEnvelopeV0(socket, type, payload) {\n try {\n if (socket?.readyState === 1) socket.send(JSON.stringify(createEnvelope(type, payload)));\n } catch {\n /* noop */\n }\n}\n\nfunction decodeRhizohVoiceLiveChunkV0(payload = {}) {\n const b64 = String(payload.audioBase64 || \"\").trim();\n if (!b64) return { ok: false, error: \"audio_base64_required\" };\n try {\n const buf = Buffer.from(b64, \"base64\");\n if (!buf.length) return { ok: false, error: \"audio_decode_empty\" };\n return { ok: true, buffer: buf };\n } catch {\n return { ok: false, error: \"audio_base64_invalid\" };\n }\n}\n\nfunction handleRhizohVoiceLiveStartV0(socket, payload = {}) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const sessionId = String(p.sessionId || `live_${Date.now().toString(36)}`).slice(0, 96);\n rhizohVoiceLiveSessions.set(socket, {\n sessionId,\n traceId: String(p.traceId || \"\").slice(0, 128),\n mimeType: String(p.mimeType || \"audio/webm\").slice(0, 80),\n languageCode: String(p.languageCode || \"tr-TR\").slice(0, 32),\n path: String(p.path || \"both\").slice(0, 24),\n chunks: [],\n chunkIndexes: [],\n totalBytes: 0,\n nextChunkIndex: 1,\n eventSeq: 0,\n events: [],\n startedAtMs: Date.now()\n });\n appendRhizohVoiceLiveEventV0(rhizohVoiceLiveSessions.get(socket), \"VOICE_LIVE_SESSION_STARTED\", `${sessionId}:live_session`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_START, {\n ok: true,\n sessionId,\n schema: \"castle.rhizoh.voice_live.gateway_lane.v0\"\n });\n}\n\nfunction handleRhizohVoiceLiveChunkV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const sequence = validateRhizohVoiceLiveChunkPayloadV0(payload, {\n expectedChunkIndex: session.nextChunkIndex\n });\n if (!sequence.ok) {\n rhizohVoiceLiveSessions.delete(socket);\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:chunk_reject`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: sequence.error,\n expectedChunkIndex: sequence.expectedChunkIndex,\n chunkIndex: sequence.chunkIndex\n });\n return;\n }\n const decoded = decodeRhizohVoiceLiveChunkV0(payload);\n if (!decoded.ok) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: decoded.error\n });\n return;\n }\n if (\n session.chunks.length >= RHIZOH_VOICE_LIVE_MAX_CHUNKS ||\n session.totalBytes + decoded.buffer.length > RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES\n ) {\n rhizohVoiceLiveSessions.delete(socket);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_audio_too_large\"\n });\n return;\n }\n session.chunks.push(decoded.buffer);\n session.chunkIndexes.push(sequence.chunkIndex);\n session.totalBytes += decoded.buffer.length;\n session.nextChunkIndex += 1;\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_CHUNK_ACCEPTED\", `${session.sessionId}:chunk:${sequence.chunkIndex}`);\n}\n\nfunction handleRhizohVoiceLiveStopV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n rhizohVoiceLiveSessions.delete(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const lastChunkIndex = Number(payload?.lastChunkIndex);\n if (Number.isInteger(lastChunkIndex) && lastChunkIndex !== session.chunks.length) {\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:stop_mismatch`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_chunk_count_mismatch\",\n expectedChunkCount: lastChunkIndex,\n receivedChunkCount: session.chunks.length\n });\n return;\n }\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_STOP_RECEIVED\", `${session.sessionId}:stop:${session.chunks.length}`);\n const audio = Buffer.concat(session.chunks);\n if (!audio.length) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"audio_empty\"\n });\n return;\n }\n runRhizohVoiceTranscribeV3({\n path: String(payload?.path || session.path || \"both\"),\n audioBase64: audio.toString(\"base64\"),\n mimeType: String(payload?.mimeType || session.mimeType || \"audio/webm\"),\n languageCode: String(payload?.languageCode || session.languageCode || \"tr-TR\"),\n traceId: String(payload?.traceId || session.traceId || \"\"),\n sessionId: session.sessionId,\n transport: \"gateway_ws_live\"\n })\n .then((result) => {\n appendRhizohVoiceLiveEventV0(session, result?.ok ? \"VOICE_TRANSCRIBE_FINAL\" : \"VOICE_TRANSCRIBE_FAILED\", `${session.sessionId}:final`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_FINAL, {\n ...result,\n sessionId: session.sessionId,\n transportPath: \"gateway_ws_live\",\n bytes: audio.length,\n chunkCount: session.chunks.length,\n chunkSequence: Object.freeze({\n ok: true,\n firstChunkIndex: session.chunkIndexes[0] || null,\n lastChunkIndex: session.chunkIndexes[session.chunkIndexes.length - 1] || null,\n receivedChunkCount: session.chunks.length\n }),\n eventTrace: Object.freeze((session.events || []).slice(-16)),\n recordedMs: Math.max(0, Date.now() - session.startedAtMs)\n });\n })\n .catch((e) => {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_transcribe_failed\",\n detail: String(e?.message || e)\n });\n });\n}\n\nfunction removeCastleSocialClientFromAllRooms(clientId) {\n const cid = String(clientId || \"\");\n if (!cid) return;\n for (const room of castleSocialRoomState.values()) {\n room.members.delete(cid);\n }\n}\n\nfunction buildCastleSocialRoomPayload(roomKey) {\n const room = castleSocialRoomState.get(roomKey);\n if (!room) return null;\n const now = Date.now();\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const roster = [...room.members.values()].map((m) => ({\n userId: m.userId,\n lastMs: m.lastMs,\n ...m.sp\n }));\n const peerFeeds =\n room.walPeers && room.walPeers.size\n ? [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }))\n : [];\n return { castleRoomKey: roomKey, seq: room.seq, roster, peerFeeds, ts: now };\n}\n\nconst CASTLE_WAL_PEER_MAX_FEED_BYTES = 48 * 1024;\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {{ ok: boolean, code?: string, reason?: string }} gate\n */\nfunction rejectWalPeerIngressV0(socket, gate) {\n const code = gate.code || \"wal_peer_rejected\";\n recordWalPeerRejectV0(code);\n recordSubstrateOtelEventV0(\"wal_peer_reject\", { code, reason: String(gate.reason || \"\").slice(0, 120) });\n socket.send(\n JSON.stringify(\n createEnvelope(WS_MESSAGE.ERROR, {\n error: gate.reason || \"WAL peer ingress rejected.\",\n code\n })\n )\n );\n}\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {Record<string, unknown> | null | undefined} walPeerFeed\n * @returns {boolean}\n */\nfunction gateWalPeerIngressV0(socket, walPeerFeed) {\n const authGate = assertWalPeerSocketAuthorityV0(socket);\n if (!authGate.ok) {\n rejectWalPeerIngressV0(socket, authGate);\n return false;\n }\n const sigGate = validateWalPeerFeedSignatureV0(\n walPeerFeed && typeof walPeerFeed === \"object\" ? walPeerFeed : {}\n );\n if (!sigGate.ok) {\n rejectWalPeerIngressV0(socket, sigGate);\n return false;\n }\n return true;\n}\n\nfunction handleCastleWalPeerFeed(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_WAL_PEER_MAX_FEED_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_WAL_PEER_FEED too large.\" })));\n return;\n }\n const walPeerFeed = p.walPeerFeed && typeof p.walPeerFeed === \"object\" ? p.walPeerFeed : {};\n if (!gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const castleRoomKey = String(p.castleRoomKey || \"wal:convergence\").slice(0, 64);\n const castleId = String(p.castleId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId,\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: room.members.get(socket.clientId)?.sp ?? {}\n });\n const peerFeeds = [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }));\n const body = {\n castleRoomKey,\n seq: room.seq,\n peerFeeds,\n ts: now\n };\n const env = createEnvelope(WS_MESSAGE.CASTLE_WAL_PEER_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys(), ...room.walPeers.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction handleCastleSocialPulse(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_SOCIAL_MAX_PULSE_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_SOCIAL_PULSE too large.\" })));\n return;\n }\n const castleRoomKey = String(p.castleRoomKey || \"default\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n const sp = p.socialPulse && typeof p.socialPulse === \"object\" ? p.socialPulse : {};\n const walPeerFeed =\n p.walPeerFeed && typeof p.walPeerFeed === \"object\"\n ? p.walPeerFeed\n : sp.walPeerFeed && typeof sp.walPeerFeed === \"object\"\n ? sp.walPeerFeed\n : null;\n if (walPeerFeed && !gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const spNext = walPeerFeed ? { ...sp, walPeerFeed } : { ...sp };\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: spNext\n });\n if (walPeerFeed) {\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId: String(p.castleId || userId).slice(0, 64),\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n }\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const body = buildCastleSocialRoomPayload(castleRoomKey);\n if (!body) return;\n const env = createEnvelope(WS_MESSAGE.CASTLE_SOCIAL_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcast(msgObj) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcastState() {\n broadcast(\n createEnvelope(WS_MESSAGE.BROADCAST_STATE, {\n broadcasterClientId,\n viewers: [...wss.clients].filter((c) => c.readyState === 1).length\n })\n );\n}\n\nfunction currentSpiralStatePayload() {\n return {\n rooms: spiralState.rooms,\n characters: [...spiralState.characters.values()]\n };\n}\n\nfunction broadcastSpiralState() {\n broadcast(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload()));\n}\n\nfunction ensureRoom(roomId, fallbackName = null) {\n if (!roomId) return spiralState.rooms[0].id;\n const exists = spiralState.rooms.find((r) => r.id === roomId);\n if (exists) return exists.id;\n const created = { id: roomId, name: fallbackName || `Spiral-${roomId.slice(-4)}` };\n spiralState.rooms.push(created);\n return created.id;\n}\n\nfunction syncSpiralCharactersWithWorld(snapshot) {\n const agents = snapshot?.agents || [];\n for (const a of agents) {\n if (!spiralState.characters.has(a.id)) {\n spiralState.characters.set(a.id, {\n id: a.id,\n name: a.id,\n role: \"agent\",\n roomId: spiralState.rooms[0].id,\n source: \"world\"\n });\n }\n }\n}\n\nfunction broadcastExcept(msgObj, exceptSocket) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client !== exceptSocket && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction validateInputFrame(payload, socket) {\n const stats = clientStats.get(socket.clientId) || { lastFrame: 0, windowStart: Date.now(), msgCount: 0 };\n const now = Date.now();\n if (now - stats.windowStart > 5000) {\n stats.windowStart = now;\n stats.msgCount = 0;\n }\n stats.msgCount += 1;\n clientStats.set(socket.clientId, stats);\n if (stats.msgCount > 120) return \"Rate limit exceeded.\";\n\n if (!payload || !Number.isInteger(payload.frame)) return \"frame must be integer.\";\n if (payload.frame <= stats.lastFrame) return \"frame must be monotonic.\";\n const commands = Array.isArray(payload.commands) ? payload.commands : [];\n if (commands.length > 8) return \"Too many commands in one frame.\";\n for (const cmd of commands) {\n if (!cmd || typeof cmd.command !== \"string\") return \"Invalid command format.\";\n if (\n cmd.command !== \"SPAWN_AGENT\" &&\n cmd.command !== \"LIST_CASTLES\" &&\n cmd.command !== \"SPAWN_ENTITY\" &&\n cmd.command !== \"CREATE_CASTLE\"\n ) return \"Unsupported command.\";\n }\n stats.lastFrame = payload.frame;\n clientStats.set(socket.clientId, stats);\n return null;\n}\n\nfunction validateSignalPayload(payload) {\n if (!payload || typeof payload !== \"object\") return \"Invalid signaling payload.\";\n const signalType = payload.signalType;\n if (![\"OFFER\", \"ANSWER\", \"ICE\"].includes(signalType)) return \"Invalid signal type.\";\n if (typeof payload.to !== \"string\" || payload.to.length < 3) return \"Invalid signal destination.\";\n if (signalType !== \"ICE\" && JSON.stringify(payload.sdp || {}).length > 16 * 1024) return \"SDP too large.\";\n if (signalType === \"ICE\" && JSON.stringify(payload.candidate || {}).length > 6 * 1024) return \"ICE too large.\";\n return null;\n}\n\nfunction sanitizeRawMessage(raw) {\n if (!raw) return null;\n const text = raw.toString();\n if (Buffer.byteLength(text, \"utf8\") > MAX_MESSAGE_BYTES) return null;\n return text;\n}\n\nwss.on(\"connection\", async (socket, req) => {\n const origin = req?.headers?.origin || \"\";\n if (!httpCors.isWebSocketOriginAllowed(origin)) {\n socket.close(1008, \"Origin not allowed\");\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const path = req?.url || \"\";\n const query = new URL(`ws://localhost${path}`).searchParams;\n const token = query.get(\"token\");\n if (!token || token !== REQUIRED_GATEWAY_TOKEN) {\n socket.close(1008, \"Invalid gateway token\");\n return;\n }\n }\n if (REQUIRE_AUTH) {\n const result = await verifyClientToken(req);\n if (!result.ok) {\n const isLocal = origin.includes(\"localhost\") || origin.includes(\"127.0.0.1\") || !origin;\n if (!(ALLOW_DEV_ANON && isLocal)) {\n socket.close(1008, result.reason);\n return;\n }\n socket.auth = { ok: true, kind: \"dev-anon\", user: { uid: \"dev-anon\" } };\n } else {\n socket.auth = result;\n }\n }\n\n socket.clientId = `c-${Math.random().toString(36).slice(2, 8)}`;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.HELLO, { message: \"Castle Gateway online.\", clientId: socket.clientId })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.WORLD_SNAPSHOT, orchestrator.worldSnapshot())));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.BROADCAST_STATE, { broadcasterClientId, viewers: [...wss.clients].filter((c) => c.readyState === 1).length })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload())));\n broadcastExcept(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }), socket);\n\n socket.on(\"message\", (raw) => {\n const safeRaw = sanitizeRawMessage(raw);\n if (!safeRaw) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Message too large or invalid.\" })));\n return;\n }\n const parsed = safeJsonParse(safeRaw);\n if (!parsed?.type) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid message.\" })));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND_TEXT) {\n const canonical = parseTextToCommand(parsed.payload?.text);\n if (!canonical) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Unknown command text.\" })));\n return;\n }\n const result = orchestrator.applyCommand(canonical);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND) {\n const result = orchestrator.applyCommand(parsed.payload);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n }\n\n if (parsed.type === WS_MESSAGE.INPUT_FRAME) {\n const err = validateInputFrame(parsed.payload, socket);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const frame = parsed.payload.frame;\n const commands = Array.isArray(parsed.payload?.commands) ? parsed.payload.commands : [];\n orchestrator.queueInputFrame(socket.clientId, frame, commands);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, queued: true, frame })));\n }\n\n if (parsed.type === WS_MESSAGE.SIGNAL) {\n const payload = parsed.payload || {};\n const err = validateSignalPayload(payload);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const signalType = payload.signalType;\n const to = payload.to;\n for (const client of wss.clients) {\n if (client.clientId === to && client.readyState === 1) {\n client.send(JSON.stringify(createEnvelope(WS_MESSAGE.SIGNAL, { ...payload, from: socket.clientId })));\n break;\n }\n }\n }\n\n if (parsed.type === WS_MESSAGE.DRONE_TELEMETRY) {\n const payload = parsed.payload || {};\n const json = JSON.stringify(payload);\n if (Buffer.byteLength(json, \"utf8\") > 48 * 1024) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"DRONE_TELEMETRY payload too large.\" })));\n return;\n }\n broadcastExcept(createEnvelope(WS_MESSAGE.DRONE_TELEMETRY, { ...payload, relayFrom: socket.clientId }), socket);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_START) {\n handleRhizohVoiceLiveStartV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_CHUNK) {\n handleRhizohVoiceLiveChunkV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_STOP) {\n handleRhizohVoiceLiveStopV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_SOCIAL_PULSE) {\n handleCastleSocialPulse(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_WAL_PEER_FEED) {\n handleCastleWalPeerFeed(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.OPEN_DATA_QUERY) {\n queryOpenData(parsed.payload)\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: true, result })));\n })\n .catch((e) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: false, error: e.message })));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_QUERY) {\n runRhizohBrainV2({\n ...(parsed.payload || {}),\n uid: socket?.auth?.user?.uid || \"unknown\"\n })\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, result)));\n })\n .catch(async () => {\n const fallback = await runRhizohBrain(parsed.payload || {}).catch(() => ({\n ok: false,\n reply: \"Rhizoh temporary disturbance. Habitat remains stable.\"\n }));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, fallback)));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.BROADCAST_REGISTER) {\n const role = String(parsed.payload?.role || \"\");\n if (role === \"GENESIS_BROADCAST_AGENT\") {\n broadcasterClientId = socket.clientId;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, registered: role })));\n broadcastState();\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.STUDIO_CUE) {\n // Only registered broadcaster can push global cues.\n if (!broadcasterClientId || socket.clientId !== broadcasterClientId) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Only broadcaster agent can emit STUDIO_CUE.\" })));\n return;\n }\n const cue = parsed.payload || {};\n if (typeof cue !== \"object\") {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid STUDIO_CUE payload.\" })));\n return;\n }\n broadcast(createEnvelope(WS_MESSAGE.STUDIO_CUE, cue));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_JOIN_ROOM) {\n const requested = String(parsed.payload?.roomId || \"\").trim();\n const requestedName = String(parsed.payload?.roomName || \"\").trim();\n const roomId = ensureRoom(requested || `room-${Math.random().toString(36).slice(2, 7)}`, requestedName || null);\n spiralState.activeByClient.set(socket.clientId, roomId);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, roomId })));\n broadcastSpiralState();\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_CREATE_CHARACTER) {\n const name = String(parsed.payload?.name || \"\").trim().slice(0, 32) || `AI-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;\n const role = String(parsed.payload?.role || \"agent\").trim().slice(0, 24) || \"agent\";\n const roomId = ensureRoom(\n String(parsed.payload?.roomId || spiralState.activeByClient.get(socket.clientId) || spiralState.rooms[0].id),\n null\n );\n\n // Character creation always maps to an authoritative world agent.\n const result = orchestrator.applyCommand({\n command: COMMAND.SPAWN_AGENT,\n params: { targetCastleId: \"castle-01\" }\n });\n if (!result?.ok || !result?.data?.id) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Character creation failed.\" })));\n return;\n }\n const id = result.data.id;\n spiralState.characters.set(id, { id, name, role, roomId, source: \"spawned\" });\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, characterId: id, roomId })));\n broadcastSpiralState();\n return;\n }\n });\n\n socket.on(\"close\", () => {\n if (socket.clientId === broadcasterClientId) broadcasterClientId = null;\n clientStats.delete(socket.clientId);\n rhizohVoiceLiveSessions.delete(socket);\n spiralState.activeByClient.delete(socket.clientId);\n removeCastleSocialClientFromAllRooms(socket.clientId);\n broadcast(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }));\n broadcastState();\n });\n});\n\nsetInterval(() => {\n void (async () => {\n try {\n const { frame, snapshot, delta, packedDelta } = orchestrator.tick(1 / 20);\n syncSpiralCharactersWithWorld(snapshot);\n broadcast(createEnvelope(WS_MESSAGE.NETWORK_TICK, { frame }));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_TICK, snapshot));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA, delta));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA_PACKED, packedDelta));\n broadcastSpiralState();\n await runAcademyEventTick();\n const executed = await tickAutomations(Date.now());\n for (const a of executed) {\n await addTranscript(a.uid, {\n source: \"planner\",\n eventType: \"automation-executed\",\n text: `Automation executed: ${a.title} (${a.action})`,\n roomId: \"studio-main\",\n meta: { automationId: a.id, action: a.action, deviceId: a.deviceId || \"\" }\n });\n }\n } catch (e) {\n console.error(\"[GATEWAY] world tick / automations loop error:\", String(e?.message || e));\n }\n })();\n}, 50);\n\n(async () => {\n await rhizohRuntime.telemetry.initOpenTelemetry();\n if (String(process.env.CASTLE_GENESIS_DISK_PERSIST ?? \"\").trim() === \"\") {\n process.env.CASTLE_GENESIS_DISK_PERSIST = \"1\";\n }\n const genesisDiskBoot = await hydrateGenesisContinuityPersistenceBootV0();\n if (!genesisDiskBoot.ok) {\n console.warn(\"[GATEWAY] genesis disk hydrate failed; continuing with in-memory genesis continuity\");\n }\n httpServer.listen(PORT, () => {\n startGenesisCanonicalClock();\n installGenesisCheckpointSurfaceGetter(buildGenesisRuntimeSurfacePayloadLive);\n setGenesisContinuityAfterPublishHook((seq) => {\n noteGenesisCheckpointSeqCommitted(seq);\n noteGenesisContinuitySeqAcceptedV0(seq);\n });\n startGenesisContinuityInfraSampler(buildGenesisRuntimeSurfacePayloadLive, 2000);\n logProductionObservatorySurfaceGuardsV0();\n logGatewaySubstrateAuthorityGuardsV0();\n if (isLlmWorkerEnabledV0()) {\n try {\n const workerBoot = initLlmWorkerAtBootV0();\n console.log(\"[GATEWAY] llm worker boot:\", workerBoot);\n } catch (workerErr) {\n console.error(\"[GATEWAY] llm worker boot failed:\", workerErr);\n }\n }\n console.log(`[GATEWAY] ws/http://localhost:${PORT}`);\n console.log(\n `[GATEWAY] genesis continuity: GET ${rhizohRuntime.routes.genesisRuntime} | POST ${rhizohRuntime.routes.genesisIngress} | SSE ${rhizohRuntime.routes.genesisStream} | replay ${rhizohRuntime.routes.genesisReplay} | diff ${rhizohRuntime.routes.genesisReplayDiff} | equiv ${rhizohRuntime.routes.genesisReplayEquivalence} | analytics ${rhizohRuntime.routes.genesisReplayAnalytics} | evolution ${rhizohRuntime.routes.genesisReplayEvolution}`\n );\n if (String(process.env.CASTLE_GENESIS_OBSERVATORY_LIVE_V01 ?? \"\").trim() === \"1\") {\n console.log(\"GENESIS OBSERVATORY LIVE v0.1 — READ ONLY PHASE STARTED\");\n }\n });\n})().catch((e) => {\n console.error(\"[GATEWAY] fatal boot (before or during listen):\", e);\n process.exit(1);\n});\n"}, "61234d5bc7b91ef419ded43d11fdf0c6ce454194": {"t": 1785754148.0917912, "n": 166932, "h": "5b357b0ea9877152", "c": "import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport dotenv from \"dotenv\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconsole.log(\"🔥 [GENESIS_BOOT] entry file:\", __filename);\nconsole.log(\"🔥 [GENESIS_BOOT] node env:\", process.env.NODE_ENV);\nconsole.log(\"🔥 [GENESIS_BOOT] pid:\", process.pid);\nconsole.log(\"🧬 [GENESIS_BOOT] cwd:\", process.cwd());\nconsole.log(\"🧬 [GENESIS_BOOT] import.meta.url:\", import.meta.url);\ndotenv.config({ path: path.join(__dirname, \"..\", \".env\") });\ndotenv.config({ path: path.join(__dirname, \"..\", \".env.local\"), override: true });\n\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport { createServer } from \"node:http\";\nimport { createOrchestrator } from \"@castle/orchestrator\";\nimport { parseTextToCommand } from \"@castle/command-dsl\";\nimport {\n WS_MESSAGE,\n COMMAND,\n createEnvelope,\n createRhizohImmutableEventV0,\n createRhizohPayloadRefV0,\n safeJsonParse,\n validateRhizohVoiceLiveChunkPayloadV0\n} from \"@castle/protocol\";\nimport { queryOpenData } from \"./openData.js\";\nimport { verifyClientToken } from \"./auth.js\";\nimport { runRhizohBrain } from \"./rhizohBrain.js\";\nimport { queryRhizohLlm } from \"./rhizohLlmGateway.js\";\nimport { rhizohGatewayTurn } from \"./rhizohGatewayTurn.js\";\nimport {\n enqueueLlmWorkerTaskAsyncV0,\n getLatestLlmWorkerTaskIdV0,\n getLlmWorkerDebugSnapshotV0,\n getLlmWorkerTaskSnapshotV0,\n initLlmWorkerAtBootV0,\n isLlmWorkerAsyncEnabledV0,\n isLlmWorkerEnabledV0,\n runLlmWorkerTaskV0\n} from \"./llmWorkerRuntime.js\";\nimport {\n RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n runRhizohVoiceTranscribeV3,\n rhizohVoiceTranscribeEnvV3\n} from \"./rhizohVoiceTranscribeV3.js\";\nimport { createHttpCorsPolicy, normalizeHttpOrigin, parseAllowedOriginsFromEnv } from \"./httpCorsPolicyV1.js\";\nimport {\n applyLanguagePropagationToLlmPayloadV1,\n applyLanguagePropagationToVoiceBodyV1,\n buildRhizohLanguagePropagationEchoV1,\n parseRhizohLanguagePropagationV1\n} from \"./rhizohLanguagePropagationV1.js\";\nimport { trimRhizohLlmGatewayPayloadV1 } from \"./rhizohLlmPayloadTrimV1.js\";\nimport {\n meshAppendDelta,\n meshContinuityAggregate,\n meshJoin,\n meshLeave,\n meshReplayFromNodeId,\n meshReplayFromSeq,\n meshSnapshot,\n meshSubscribeSse\n} from \"./presenceRoomMesh.js\";\nimport { runRhizohBrainV2 } from \"./rhizohBrainV2.js\";\nimport {\n listConnections,\n createConnection,\n updateConnection,\n deleteConnection,\n setDefaultConnection,\n resolveConnection\n} from \"./llmConnectionsStore.js\";\nimport { checkHttpRateLimit, getHttpClientIp } from \"./castleHttpRateLimit.js\";\nimport {\n submitAbuseReportV0,\n listModerationQueueV0,\n softBlockUserV0,\n removeSoftBlockV0,\n listSoftBlockedUsersV0\n} from \"./ops/moderationMvpV0.js\";\nimport { listRecentAgentSnapshotsV0 } from \"./ops/agentObservabilityV0.js\";\nimport { readAgentContainmentConfigV0 } from \"./ops/agentContainmentV0.js\";\nimport { readCostContainmentConfigV0, getCostContainmentStatsV0 } from \"./ops/costContainmentV0.js\";\nimport { getGlobalCostLedgerSnapshotV0, getGclBackendStatusV0 } from \"./ops/globalCostLedgerV0.js\";\nimport { evaluateEconomicStrategyL3V0 } from \"./ops/economicStrategyEngineL3V0.js\";\nimport { getPhasedRolloutStatsV0, readPhasedRolloutTierV0 } from \"./ops/phasedRolloutV0.js\";\nimport { buildUnifiedStateNarrativeV0 } from \"./ops/unifiedStateNarrativeV0.js\";\nimport { resolveHardeningTenantIdV0 } from \"./ops/narrativeTenantIsolationV0.js\";\nimport {\n classifyStressResponseV0,\n verifyStressTaxonomyCoverageV0,\n verifyStressConflictResolutionV0\n} from \"./ops/stressResponseTaxonomyV0.js\";\nimport { verifyResolutionStabilityEnvelopeV0 } from \"./ops/resolutionStabilityEnvelopeV0.js\";\nimport { logLlmAccess } from \"./castleLlmAudit.js\";\nimport { appendMemory, listMemories, getMemoryContext, getPersonaGoalMemory, setPersonaGoalMemory, autoCompactMemories } from \"./memoryStore.js\";\nimport { getFirebasePersistence } from \"./firebasePersistence.js\";\nimport { registerAgentIdentity, listAgentIdentities, getAgentIdentity, updateAgentIdentity } from \"./agentIdentityStore.js\";\nimport { queueAcademyEvent, listAcademyEvents, resolveAcademyEvent, runAcademyEventTick } from \"./academyEventEngine.js\";\nimport { enqueuePdfJob, getPdfJob } from \"./pdfPipeline.js\";\nimport {\n createPublishSession,\n listPublishSessions,\n updatePublishSession,\n addTranscript,\n listTranscripts,\n searchMetadata,\n registerDevice,\n listDevices,\n updateDevice,\n appendTelemetry,\n listTelemetry,\n createAutomation,\n listAutomations,\n tickAutomations,\n enqueueCommand,\n listCommands,\n updateCommandStatus,\n registerSocialChannel,\n listSocialChannels\n} from \"./studioOpsStore.js\";\nimport { metrics as infraMetrics } from \"./infra/metrics.js\";\nimport { renderRhizohPrometheusMetrics, rhizohEnterpriseMetrics } from \"./infra/rhizohEnterpriseMetrics.js\";\nimport { scoreHealth } from \"./infra/healthScore.js\";\nimport { getTraceById, listTracesBySession } from \"./infra/traceRegistry.js\";\nimport { canonicalEpistemicSealString, hashAndSignEpistemicSeal, EPISTEMIC_SEAL_SCHEMA } from \"./epistemicSeal.js\";\nimport { persistEpistemicLedgerBatch } from \"./epistemicLedgerStore.js\";\nimport { persistEpistemicForecastBatch } from \"./epistemicForecastStore.js\";\nimport { buildRhizohExternalGroundTruthPayload } from \"./rhizohExternalGroundTruthGateway.js\";\nimport { ingestRhizohExternalLossBatchHttp } from \"./rhizohExternalLossIngestGateway.js\";\nimport {\n ingestRhizohProductOutcomeHttp,\n listRhizohProductOutcomeAggregates,\n verifyRhizohOutcomeHmac,\n verifyRhizohOutcomeSourceToken\n} from \"./rhizohProductOutcomeIngestGateway.js\";\nimport { initRhizoh } from \"./rhizohProductionBootstrap.js\";\nimport { handleAcademicObservatoryExportGetV0 } from \"./rhizoh/academicObservatoryHttpV0.js\";\nimport {\n handleLiveSportsBundleGetV0,\n handleLiveNewsHeadlinesGetV0,\n handleLiveWorldFeedGetV0\n} from \"./rhizoh/liveWorldFeedHttpV0.js\";\nimport {\n buildGenesisRuntimeSurfacePayload,\n recordGenesisEpistemicLedgerPersisted,\n recordGenesisEpistemicSealIssued,\n startGenesisCanonicalClock\n} from \"./genesisRuntimeSurfaceV0.js\";\nimport { registerGenesisSseClient, getGenesisContinuitySeq, setGenesisContinuityAfterPublishHook } from \"./genesisContinuityStreamHubV0.js\";\nimport { computeGenesisReplayFingerprintV0 } from \"./genesisReplayFingerprintV0.js\";\nimport { buildGenesisGatewayCapabilitiesV0 } from \"./genesisGatewayCapabilitiesV0.js\";\nimport {\n installGenesisCheckpointSurfaceGetter,\n noteGenesisCheckpointSeqCommitted,\n getLatestGenesisSignedCheckpoint\n} from \"./genesisContinuityCheckpointV0.js\";\nimport { hydrateGenesisContinuityPersistenceBootV0 } from \"./genesisContinuityHydrateBootV0.js\";\nimport { startGenesisContinuityInfraSampler } from \"./genesisContinuityInfraSamplerV0.js\";\nimport {\n genesisCheckpointQueryBySeqV0,\n genesisCheckpointQueryRangeV0,\n genesisCheckpointQueryLineageV0\n} from \"./genesisCheckpointQueryV0.js\";\nimport {\n genesisContinuityEventArchiveEnabled,\n queryGenesisContinuityEventArchiveV0,\n GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION\n} from \"./genesisContinuityEventArchiveV0.js\";\nimport { resolveGenesisReplayRouterV1, GENESIS_REPLAY_ROUTER_SCHEMA } from \"./genesisReplayRouterV1.js\";\nimport {\n computeGenesisReplayTemporalDiffV1,\n GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA\n} from \"./genesisReplayTemporalDiffV1.js\";\nimport { compareGenesisReplayEquivalenceV1 } from \"./genesisReplayEquivalenceV1.js\";\nimport { computeGenesisReplayAnalyticsV1 } from \"./genesisReplayAnalyticsV1.js\";\nimport { computeGenesisReplayEvolutionV1 } from \"./genesisReplayEvolutionV1.js\";\nimport { handleStripeCheckoutCreate, handleStripeMembershipWebhook } from \"./stripeWebhookMembership.js\";\nimport { buildRealityHealthPayload } from \"./edgeRealityConsistencyV1.js\";\nimport {\n assertWalPeerSocketAuthorityV0,\n buildGatewaySubstrateAuthoritySnapshotV0,\n logGatewaySubstrateAuthorityGuardsV0,\n validateWalPeerFeedSignatureV0\n} from \"./gatewaySubstrateAuthorityV0.js\";\nimport {\n buildSubstrateOperationalSnapshotV0,\n recordWalPeerRejectV0,\n renderSubstratePrometheusMetricsV0\n} from \"./infra/substrateOperationalMetrics.js\";\nimport { ingestClientSubstrateHealthV0 } from \"./rhizohSubstrateHealthIngestV0.js\";\nimport { recordSubstrateOtelEventV0 } from \"./infra/opentelemetrySubstrateV0.js\";\nimport {\n GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n getGenesisWorldObservationIngressStatusV0,\n ingestGenesisWorldObservationV0\n} from \"./genesisContinuityClientIngressV0.js\";\nimport {\n getGenesisContinuitySeqAuditV0,\n noteGenesisContinuitySeqAcceptedV0\n} from \"./genesisContinuitySeqGapV0.js\";\n\n/** Render / Fly / Railway: `PORT` — yoksa `CASTLE_GATEWAY_PORT` — yoksa 8090. */\nconst PORT =\n Number(process.env.PORT) ||\n Number(process.env.CASTLE_GATEWAY_PORT) ||\n 8090;\n/** Spiral / world WebSocket server — set after `wss` init; used by genesis runtime surface. */\nlet spiralWssForGenesis = null;\nconst MAX_MESSAGE_BYTES = Number(process.env.CASTLE_MAX_MESSAGE_BYTES || 32 * 1024);\nconst REQUIRED_GATEWAY_TOKEN = process.env.CASTLE_GATEWAY_TOKEN || \"\";\nconst ALLOWED_ORIGINS = parseAllowedOriginsFromEnv().fromList;\nconst httpCors = createHttpCorsPolicy();\n\n/** @returns {string|null} `Access-Control-Allow-Origin`; null = browser CORS fail. */\nfunction accessControlAllowOriginValue(req) {\n return httpCors.accessControlAllowOriginValue(req);\n}\n\nfunction applyHttpCorsHeaders(req, res) {\n return httpCors.applyHttpCorsHeaders(req, res);\n}\nconst REQUIRE_AUTH = process.env.CASTLE_REQUIRE_AUTH === \"true\";\nconst ALLOW_DEV_ANON = process.env.CASTLE_ALLOW_DEV_ANON !== \"false\";\nconst ALLOW_DEV_HTTP_UID = process.env.CASTLE_ALLOW_DEV_HTTP_UID !== \"false\";\n/** Presence mesh: allow `X-Castle-Guest-Id` when Firebase auth absent (default on). Set `false` to require auth only. */\nconst ALLOW_MESH_GUEST = process.env.CASTLE_ALLOW_MESH_GUEST !== \"false\";\nconst SOCIAL_RETRY_MAX = Math.max(0, Math.min(5, Number(process.env.CASTLE_SOCIAL_RETRY_MAX || 2)));\nconst SOCIAL_RETRY_BASE_MS = Math.max(100, Number(process.env.CASTLE_SOCIAL_RETRY_BASE_MS || 700));\nconst TELEGRAM_BOT_TOKEN = process.env.CASTLE_TELEGRAM_BOT_TOKEN || \"\";\nconst WHATSAPP_TOKEN = process.env.CASTLE_WHATSAPP_TOKEN || \"\";\nconst WHATSAPP_PHONE_NUMBER_ID = process.env.CASTLE_WHATSAPP_PHONE_NUMBER_ID || \"\";\nconst RL_RHIZOH_LLM_PER_MIN = Math.max(5, Number(process.env.CASTLE_RL_RHIZOH_LLM_PER_MIN || 40));\nconst RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN || 30)\n);\nconst RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN || 120));\nconst RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN = Math.max(\n 10,\n Number(process.env.CASTLE_RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN || 60)\n);\nconst RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN = Math.max(8, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN || 48));\nconst RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN = Math.max(4, Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN || 24));\nconst RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN || 18)\n);\nconst RL_EPISTEMIC_SEAL_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_EPISTEMIC_SEAL_PER_MIN || 120));\nconst RL_EPISTEMIC_LOG_PER_MIN = Math.max(20, Number(process.env.CASTLE_RL_EPISTEMIC_LOG_PER_MIN || 240));\nconst EPISTEMIC_SEAL_SECRET = String(\n process.env.CASTLE_EPISTEMIC_SEAL_SECRET || process.env.CASTLE_GATEWAY_TOKEN || \"\"\n).trim();\nconst RL_LLM_CONN_TEST_PER_MIN = Math.max(2, Number(process.env.CASTLE_RL_LLM_CONNECTION_TEST_PER_MIN || 8));\n\n/** One-shot production warnings: CORS surface, auth toggle, dev bypass flags (observatory deploy hygiene). */\nfunction logProductionObservatorySurfaceGuardsV0() {\n if (String(process.env.NODE_ENV || \"\").toLowerCase() !== \"production\") return;\n const tag = \"[GATEWAY_PROD_GUARD]\";\n const primaryCors = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n const corsListEmpty = ALLOWED_ORIGINS.length === 0;\n if (corsListEmpty && !primaryCors) {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty and CASTLE_HTTP_CORS_ORIGIN empty — production still merges RHIZOH_BROWSER_ORIGINS_V1 defaults; set env for preview/staging hosts`\n );\n } else if (corsListEmpty && primaryCors && primaryCors !== \"*\") {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty — only CASTLE_HTTP_CORS_ORIGIN set; add web.app + firebaseapp.com (and previews) to CASTLE_ALLOWED_ORIGINS if clients use alternate hostnames`\n );\n }\n if (primaryCors && /localhost|127\\.0\\.0\\.1/i.test(primaryCors)) {\n console.warn(`${tag} CORS: CASTLE_HTTP_CORS_ORIGIN is localhost while NODE_ENV=production`);\n }\n if (REQUIRE_AUTH) {\n console.warn(\n `${tag} AUTH: CASTLE_REQUIRE_AUTH=true — clients must send a valid gateway credential (otherwise silent 401 on replay/SSE); EventSource cannot set custom headers unless proxied`\n );\n if (REQUIRED_GATEWAY_TOKEN.length < 16) {\n console.warn(`${tag} AUTH: CASTLE_GATEWAY_TOKEN missing or short while require-auth enabled`);\n }\n }\n if (ALLOW_DEV_ANON) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_ANON is not \"false\" — dev anonymous path may be live in production (set CASTLE_ALLOW_DEV_ANON=false)`\n );\n }\n if (ALLOW_DEV_HTTP_UID) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_HTTP_UID is not \"false\" — X-Castle-Dev-Uid bypass risk (set CASTLE_ALLOW_DEV_HTTP_UID=false)`\n );\n }\n}\n\nconst rhizohRuntime = initRhizoh();\n\n/** İstemci: llmKeySource veya keySource — env | user_connection | auto */\nfunction normalizeClientLlmKeySource(raw) {\n const s = String(raw ?? \"\").trim().toLowerCase();\n if (s === \"env\" || s === \"server\") return \"env\";\n if (s === \"user_connection\" || s === \"connection\" || s === \"user\") return \"user_connection\";\n if (s === \"auto\" || s === \"\") return \"auto\";\n return null;\n}\n\nfunction sendJson(res, statusCode, payload) {\n res.writeHead(statusCode, { \"Content-Type\": \"application/json; charset=utf-8\" });\n res.end(JSON.stringify(payload));\n}\n\nfunction logRhizohHealth(stage, detail = {}) {\n try {\n console.info(`[CASTLE_${String(stage || \"unknown\")}]`, detail && typeof detail === \"object\" ? detail : {});\n } catch {\n /* noop */\n }\n}\n\nfunction renderInfraMetricsProm() {\n const lines = [\n `castle_gateway_events_processed_total ${infraMetrics.eventsProcessed}`,\n `castle_gateway_duplicate_reject_total ${infraMetrics.duplicateRejects}`,\n `castle_gateway_enqueue_latency_ms ${infraMetrics.enqueueLatencyMs}`,\n `castle_gateway_queue_depth ${infraMetrics.queueDepth}`,\n `castle_gateway_queue_lag_seconds ${(Number(infraMetrics.queueLag || 0) / 1000).toFixed(3)}`,\n `castle_gateway_errors_total ${infraMetrics.errors}`\n ];\n const buckets = infraMetrics.enqueueLatencyBuckets || {};\n for (const k of Object.keys(buckets)) {\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"${k}\"} ${buckets[k]}`);\n }\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"+Inf\"} ${infraMetrics.eventsProcessed}`);\n if (process.env.CASTLE_SUBSTRATE_METRICS !== \"0\") {\n lines.push(renderSubstratePrometheusMetricsV0().trimEnd());\n }\n return lines.join(\"\\n\");\n}\n\nasync function fetchJsonWithTimeout(url, timeoutMs = 1200) {\n const ctl = new AbortController();\n const tid = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const res = await fetch(url, { signal: ctl.signal });\n if (!res.ok) return null;\n return await res.json();\n } catch {\n return null;\n } finally {\n clearTimeout(tid);\n }\n}\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction getHttpPathname(req) {\n let p = String(req.url || \"\").split(\"?\")[0];\n /* Reverse proxies / clients sometimes append `/` — avoid silent 404 on e.g. `/rhizoh/genesis/stream/`. */\n while (p.length > 1 && p.endsWith(\"/\")) {\n p = p.slice(0, -1);\n }\n return p;\n}\n\nfunction rhizohLlmEnvConfigured() {\n const p = String(process.env.CASTLE_LLM_PROVIDER || \"openai\").toLowerCase();\n const key =\n p === \"anthropic\"\n ? process.env.ANTHROPIC_API_KEY\n : p === \"gemini\"\n ? process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY\n : p === \"xai\"\n ? process.env.XAI_API_KEY\n : p === \"deepseek\"\n ? process.env.DEEPSEEK_API_KEY\n : p === \"mistral\"\n ? process.env.MISTRAL_API_KEY\n : p === \"openrouter\"\n ? process.env.OPENROUTER_API_KEY\n : process.env.OPENAI_API_KEY;\n return !!String(key || \"\").trim();\n}\n\n/**\n * @param {import(\"firebase-admin/firestore\").Firestore | null} db\n * @param {number} timeoutMs\n */\nasync function probeFirestoreReachable(db, timeoutMs = 2000) {\n if (!db) return { ok: false, reason: \"no_db\" };\n const timeout = sleep(timeoutMs).then(() => ({ ok: false, reason: \"timeout\" }));\n const work = db\n .collection(\"_castle_gateway_health\")\n .doc(\"ping\")\n .get()\n .then(() => ({ ok: true, reason: \"reachable\" }))\n .catch((e) => ({ ok: false, reason: String(e?.message || e || \"firestore_error\") }));\n return Promise.race([work, timeout]);\n}\n\nfunction stripHtml(text = \"\") {\n return String(text)\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \" \")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \" \")\n .replace(/<[^>]+>/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction haversineMeters(aLat, aLon, bLat, bLon) {\n const R = 6371000;\n const dLat = ((bLat - aLat) * Math.PI) / 180;\n const dLon = ((bLon - aLon) * Math.PI) / 180;\n const sa = Math.sin(dLat / 2);\n const sb = Math.sin(dLon / 2);\n const x = sa * sa + Math.cos((aLat * Math.PI) / 180) * Math.cos((bLat * Math.PI) / 180) * sb * sb;\n return 2 * R * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));\n}\n\nasync function checkPublishTargetHealth(protocol, target) {\n const p = String(protocol || \"\").toUpperCase();\n const t = String(target || \"\").trim();\n if (!t) return { ok: false, reason: \"target_missing\", shouldReconnect: false };\n if (p === \"RTMP\") {\n const ok = t.startsWith(\"rtmp://\") || t.startsWith(\"rtmps://\");\n return { ok, reason: ok ? \"rtmp_target_format_ok\" : \"invalid_rtmp_target\", shouldReconnect: !ok, reconnectInSec: 15 };\n }\n if (p === \"WHIP\" || p === \"SFU\") {\n try {\n const res = await fetch(t, { method: \"OPTIONS\" });\n const ok = res.status < 500;\n return { ok, reason: `http_${res.status}`, shouldReconnect: !ok, reconnectInSec: 8 };\n } catch {\n return { ok: false, reason: \"network_unreachable\", shouldReconnect: true, reconnectInSec: 8 };\n }\n }\n return { ok: true, reason: \"unchecked_protocol\", shouldReconnect: false };\n}\n\nasync function fetchTextFromUrl(url) {\n const res = await fetch(url, { method: \"GET\" });\n if (!res.ok) throw new Error(`fetch_${res.status}`);\n const ct = String(res.headers.get(\"content-type\") || \"\");\n if (ct.includes(\"application/pdf\")) throw new Error(\"pdf_binary_not_supported_directly\");\n const body = await res.text();\n return stripHtml(body).slice(0, 14000);\n}\n\nfunction buildTaskActionGraph(task = \"\", context = {}) {\n const t = String(task || \"\").toLowerCase();\n if (t.includes(\"dinner\") || t.includes(\"akşam yemeği\") || t.includes(\"aksam yemegi\")) {\n return {\n taskTitle: \"Dinner Prep Planner\",\n nodes: [\n { id: \"n1\", title: \"Kitchen preheat\", action: \"smart-oven.preheat\", params: { temperatureC: 190, mode: \"fan\" } },\n { id: \"n2\", title: \"Lighting comfort\", action: \"smart-light.set_scene\", params: { scene: \"warm_dinner\" } },\n { id: \"n3\", title: \"Fridge inventory check\", action: \"smart-fridge.inventory_scan\", params: {} },\n { id: \"n4\", title: \"Reminder notification\", action: \"smart-watch.notify\", params: { text: \"Dinner prep started.\" } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" },\n { from: \"n3\", to: \"n4\" }\n ]\n };\n }\n return {\n taskTitle: task || \"General Planner\",\n nodes: [\n { id: \"n1\", title: \"Safety check\", action: \"device.safety_check\", params: { context } },\n { id: \"n2\", title: \"Primary action\", action: \"device.execute_task\", params: { task } },\n { id: \"n3\", title: \"Report\", action: \"studio.timeline_note\", params: { task } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" }\n ]\n };\n}\n\nfunction buildEthicsPrograms() {\n return {\n policyVersion: \"v1\",\n tracks: [\n {\n id: \"elderly-care-assist\",\n audience: \"elderly\",\n title: \"Elderly Assistance Track\",\n principles: [\"human-in-the-loop\", \"consent-first\", \"low-friction alerts\"],\n modules: [\"medication-reminder\", \"fall-risk-check\", \"family-notify\", \"daily-companion-brief\"]\n },\n {\n id: \"kids-safe-learning\",\n audience: \"children\",\n title: \"Kids Safe Learning Track\",\n principles: [\"guardian-consent\", \"age-appropriate-language\", \"content-filtering\"],\n modules: [\"study-coach\", \"screen-time-balance\", \"safe-social-share\", \"emergency-contact\"]\n }\n ]\n };\n}\n\nasync function deliverViaTelegram(channel, text) {\n const chatId = String(channel.endpoint || \"\").trim();\n if (!TELEGRAM_BOT_TOKEN) throw new Error(\"telegram_token_missing\");\n if (!chatId) throw new Error(\"telegram_chat_id_missing\");\n const url = `https://api.telegram.org/bot${encodeURIComponent(TELEGRAM_BOT_TOKEN)}/sendMessage`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ chat_id: chatId, text })\n });\n if (!res.ok) throw new Error(`telegram_http_${res.status}`);\n return { provider: \"telegram\", status: res.status };\n}\n\nasync function deliverViaWhatsapp(channel, text) {\n const to = String(channel.endpoint || \"\").trim();\n if (!WHATSAPP_TOKEN) throw new Error(\"whatsapp_token_missing\");\n if (!WHATSAPP_PHONE_NUMBER_ID) throw new Error(\"whatsapp_phone_number_id_missing\");\n if (!to) throw new Error(\"whatsapp_to_missing\");\n const url = `https://graph.facebook.com/v22.0/${encodeURIComponent(WHATSAPP_PHONE_NUMBER_ID)}/messages`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${WHATSAPP_TOKEN}` },\n body: JSON.stringify({ messaging_product: \"whatsapp\", to, type: \"text\", text: { body: text } })\n });\n if (!res.ok) throw new Error(`whatsapp_http_${res.status}`);\n return { provider: \"whatsapp\", status: res.status };\n}\n\nasync function deliverViaWebhook(channel, text) {\n const endpoint = String(channel.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"webhook_endpoint_missing\");\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text, platform: channel.platform || \"webhook\", ts: Date.now() })\n });\n if (!res.ok) throw new Error(`webhook_http_${res.status}`);\n return { provider: \"webhook\", status: res.status };\n}\n\nasync function deliverSocialMessage(channel, text) {\n const platform = String(channel.platform || \"webhook\").toLowerCase();\n if (platform === \"telegram\") return deliverViaTelegram(channel, text);\n if (platform === \"whatsapp\") return deliverViaWhatsapp(channel, text);\n return deliverViaWebhook(channel, text);\n}\n\nasync function deliverSocialWithRetry(channel, text) {\n let attempt = 0;\n let lastError = null;\n while (attempt <= SOCIAL_RETRY_MAX) {\n try {\n const result = await deliverSocialMessage(channel, text);\n return { ok: true, attempt: attempt + 1, ...result };\n } catch (error) {\n lastError = error;\n if (attempt >= SOCIAL_RETRY_MAX) break;\n const jitter = Math.floor(Math.random() * 90);\n await sleep(SOCIAL_RETRY_BASE_MS * Math.pow(2, attempt) + jitter);\n }\n attempt += 1;\n }\n return { ok: false, attempt: attempt + 1, error: String(lastError?.message || \"delivery_failed\") };\n}\n\nasync function dispatchCommandToAdapter(device, command) {\n const adapter = String(command?.adapter || device?.adapter || \"websocket\").toLowerCase();\n const endpoint = String(device?.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"device_endpoint_missing\");\n if (adapter === \"mqtt\") {\n // MQTT delivery is proxied via a bridge endpoint (HTTP webhook style).\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n type: \"mqtt-command\",\n topic: `castle/devices/${device.id}/commands`,\n action: command.action,\n params: command.params || {}\n })\n });\n if (!res.ok) throw new Error(`mqtt_bridge_http_${res.status}`);\n return { ok: true, adapter, via: \"http-bridge\", status: res.status };\n }\n if (!endpoint.startsWith(\"ws://\") && !endpoint.startsWith(\"wss://\")) throw new Error(\"invalid_websocket_endpoint\");\n const sendResult = await new Promise((resolve, reject) => {\n const ws = new WebSocket(endpoint);\n const timer = setTimeout(() => {\n try {\n ws.close();\n } catch {\n /* noop */\n }\n reject(new Error(\"websocket_timeout\"));\n }, 3000);\n ws.on(\"open\", () => {\n ws.send(\n JSON.stringify({\n type: \"castle-command\",\n commandId: command.id,\n deviceId: command.deviceId,\n action: command.action,\n params: command.params || {}\n }),\n (err) => {\n clearTimeout(timer);\n ws.close();\n if (err) reject(new Error(\"websocket_send_failed\"));\n else resolve({ ok: true, adapter: \"websocket\", via: \"ws\" });\n }\n );\n });\n ws.on(\"error\", () => {\n clearTimeout(timer);\n reject(new Error(\"websocket_connect_failed\"));\n });\n });\n return sendResult;\n}\n\nfunction readHttpJson(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => {\n if (!buf) return resolve({});\n const payload = safeJsonParse(buf);\n if (!payload || typeof payload !== \"object\") return reject(new Error(\"invalid_json_payload\"));\n resolve(payload);\n });\n req.on(\"error\", reject);\n });\n}\n\n/** Ham gövde (HMAC doğrulaması için). */\nfunction readHttpBodyText(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => resolve(buf));\n req.on(\"error\", reject);\n });\n}\n\nfunction maskOpaqueId(value) {\n const t = String(value || \"\");\n if (!t) return \"(empty)\";\n if (t.length <= 8) return \"****\";\n return `${t.slice(0, 4)}…${t.slice(-2)}`;\n}\n\nfunction readGatewayHttpToken(req) {\n const x = String(req.headers?.[\"x-castle-gateway-token\"] || \"\").trim();\n if (x) return x;\n const auth = String(req.headers?.authorization || \"\");\n const m = /^Bearer\\s+(.+)$/i.exec(auth);\n return m ? String(m[1] || \"\").trim() : \"\";\n}\n\nasync function resolveHttpUser(req) {\n const authResult = await verifyClientToken(req);\n if (authResult?.ok && authResult.user?.uid) {\n return { ok: true, uid: String(authResult.user.uid), mode: authResult.kind || \"auth\" };\n }\n if (ALLOW_DEV_ANON && ALLOW_DEV_HTTP_UID) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) return { ok: true, uid: `dev-${devUid.slice(0, 64)}`, mode: \"dev-header\" };\n }\n return { ok: false, reason: authResult?.reason || \"auth_required\" };\n}\n\n/**\n * Epistemic ledger ingest — Firebase uid OR valid transport token + X-Castle-Dev-Uid.\n * Prod disables CASTLE_ALLOW_DEV_HTTP_UID; LLM may still work while telemetry 401'd without this.\n */\nasync function resolveEpistemicIngestActor(req) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok && tok === REQUIRED_GATEWAY_TOKEN) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) {\n return { ok: true, uid: `gw-telemetry-${devUid.slice(0, 64)}`, mode: \"gateway-token-dev-uid\" };\n }\n return { ok: false, reason: \"dev_uid_required\" };\n }\n if (tok) {\n return { ok: false, reason: \"gateway_token_mismatch\" };\n }\n }\n const auth = await resolveHttpUser(req);\n if (auth.ok) return auth;\n return { ok: false, reason: auth.reason || \"auth_required\" };\n}\n\n/** Mesh actor: Firebase uid, dev header, or `guest:<uuid>` via header / `guestId` query (SSE cannot set headers). */\nasync function resolveMeshActor(req) {\n const auth = await resolveHttpUser(req);\n if (auth.ok) return { ok: true, clientUid: auth.uid, mode: auth.mode };\n if (ALLOW_MESH_GUEST) {\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const gid = String(req.headers?.[\"x-castle-guest-id\"] || q.searchParams.get(\"guestId\") || \"\").trim();\n if (gid) {\n const normalized = gid.startsWith(\"guest:\") ? gid.slice(0, 180) : `guest:${gid.slice(0, 160)}`;\n return { ok: true, clientUid: normalized, mode: \"guest-header\" };\n }\n }\n return { ok: false, reason: \"mesh_auth_required\" };\n}\n\n/** Live genesis runtime JSON (GET /rhizoh/genesis/runtime + SSE infra sampler). */\nasync function buildGenesisRuntimeSurfacePayloadLive() {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n let spiralWsActive = 0;\n if (spiralWssForGenesis) {\n for (const c of spiralWssForGenesis.clients) {\n if (c.readyState === 1) spiralWsActive += 1;\n }\n }\n const base = buildGenesisRuntimeSurfacePayload({\n infraMetrics,\n scoredHealth: scored,\n rhizohEnterpriseMetrics,\n mesh: meshContinuityAggregate(),\n workerHealth: worker,\n port: PORT,\n spiralWebSocketClientsActive: spiralWsActive\n });\n return {\n ...base,\n replayFingerprint: computeGenesisReplayFingerprintV0(base),\n gatewayCapabilities: buildGenesisGatewayCapabilitiesV0(),\n genesisStream: {\n schema: \"castle.genesis.stream_cursor.v0\",\n lastAcceptedSeq: getGenesisContinuitySeq(),\n clientIngress: {\n schema: GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n contract: \"castle.world_observation.ingress_envelope.v1\",\n postPath: rhizohRuntime.routes.genesisIngress,\n closure: getGenesisWorldObservationIngressStatusV0()\n },\n seqContinuity: getGenesisContinuitySeqAuditV0(),\n eventArchive: genesisContinuityEventArchiveEnabled()\n ? {\n enabled: true,\n projection: GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION,\n queryPath: rhizohRuntime.routes.genesisContinuityEvents\n }\n : { enabled: false },\n replayRouter: {\n schema: GENESIS_REPLAY_ROUTER_SCHEMA,\n queryPath: rhizohRuntime.routes.genesisReplay,\n queryHint: \"from=&to= or range=from-to; optional type=&checkpoints=0\",\n temporalDiffPath: rhizohRuntime.routes.genesisReplayDiff,\n temporalDiffSchema: GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA,\n equivalencePath: rhizohRuntime.routes.genesisReplayEquivalence,\n analyticsPath: rhizohRuntime.routes.genesisReplayAnalytics,\n evolutionPath: rhizohRuntime.routes.genesisReplayEvolution\n }\n }\n };\n}\n\nconst httpServer = createServer(async (req, res) => {\n const corsAllow = applyHttpCorsHeaders(req, res);\n if (req.method === \"OPTIONS\") {\n if (!corsAllow && req.headers?.origin) {\n console.warn(\"[GATEWAY_CORS] preflight rejected for origin:\", req.headers.origin);\n }\n res.writeHead(corsAllow ? 204 : 403);\n res.end();\n return;\n }\n\n const pathname = getHttpPathname(req);\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/genesis/__ping\") {\n sendJson(res, 200, {\n ok: true,\n pid: process.pid,\n entry: __filename,\n time: Date.now(),\n genesis: true\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/epistemic/__probe\") {\n sendJson(res, 200, {\n ok: true,\n epistemicIngestActorV2: true,\n routes: {\n seal: rhizohRuntime.routes.epistemicSeal,\n logsBatch: rhizohRuntime.routes.epistemicLogsBatch\n },\n hasGatewayToken: Boolean(REQUIRED_GATEWAY_TOKEN),\n gatewayTokenLen: REQUIRED_GATEWAY_TOKEN ? REQUIRED_GATEWAY_TOKEN.length : 0\n });\n return;\n }\n\n if (\n req.method === \"GET\" &&\n (pathname === \"/rhizoh/live/world-feed\" ||\n pathname === \"/rhizoh/live/sports-bundle\" ||\n pathname === \"/rhizoh/live/news-headlines\")\n ) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_live_world_feed:${ip}`, RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const out =\n pathname === \"/rhizoh/live/sports-bundle\"\n ? await handleLiveSportsBundleGetV0()\n : pathname === \"/rhizoh/live/news-headlines\"\n ? await handleLiveNewsHeadlinesGetV0(req)\n : await handleLiveWorldFeedGetV0(req);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/webhooks/stripe\") {\n await handleStripeMembershipWebhook(req, res);\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/billing/stripe/checkout\") {\n await handleStripeCheckoutCreate(req, res, { readHttpJson, resolveHttpUser, sendJson });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/live\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, live: false, dns: true, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n sendJson(res, 200, { ok: true, live: true, dns: true, service: \"castle-gateway\", ts: Date.now() });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/cors\") {\n const reqOrigin = normalizeHttpOrigin(req.headers?.origin);\n const allowOrigin = accessControlAllowOriginValue(req);\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n requestOrigin: reqOrigin || null,\n allowed: Boolean(allowOrigin),\n allowOrigin: allowOrigin || null,\n allowSetSize: httpCors.allowSet.size\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/ready\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, ready: false, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n try {\n const persistence = getFirebasePersistence();\n const llmProbe = rhizohLlmEnvConfigured();\n sendJson(res, 200, {\n ok: true,\n ready: true,\n dns: true,\n persistence: persistence.mode,\n service: \"castle-gateway\",\n gateway: \"ok\",\n llm: llmProbe ? \"ok\" : \"degraded\",\n spine: llmProbe ? \"ok\" : \"partial\"\n });\n } catch (e) {\n sendJson(res, 503, { ok: false, ready: false, reason: String(e?.message || e), service: \"castle-gateway\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/reality\") {\n const payload = buildRealityHealthPayload();\n sendJson(res, payload.ok ? 200 : 503, payload);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/substrate\") {\n const authority = buildGatewaySubstrateAuthoritySnapshotV0();\n const operational = buildSubstrateOperationalSnapshotV0();\n const drift = buildRealityHealthPayload();\n const ok = Boolean(authority.productionAuthoritySatisfied) && drift.ok;\n sendJson(res, ok ? 200 : 503, {\n ok,\n service: \"castle-gateway\",\n ts: Date.now(),\n authority,\n operational,\n drift: { ok: drift.ok, errors: drift.drift?.errors, warnings: drift.drift?.warnings }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/substrate/health\") {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const body = await readHttpJson(req);\n const uid = (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestClientSubstrateHealthV0(body, { uid, clientId: uid });\n if (!result.ok) {\n sendJson(res, 400, result);\n return;\n }\n recordSubstrateOtelEventV0(\"client_health_ingest\", { uid: String(uid).slice(0, 32) });\n sendJson(res, 200, result);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/deps\") {\n const t0 = Date.now();\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, {\n ok: false,\n dns: true,\n llm: false,\n firestore: false,\n phase: \"maintenance\",\n latencyMs: Date.now() - t0,\n service: \"castle-gateway\"\n });\n return;\n }\n const persistence = getFirebasePersistence();\n let firestoreOk = persistence.mode !== \"firebase\";\n let firestoreDetail = persistence.mode === \"file\" ? \"file_mode\" : \"not_applicable\";\n if (persistence.mode === \"firebase\" && persistence.db) {\n const pr = await probeFirestoreReachable(persistence.db, 2000);\n firestoreOk = pr.ok;\n firestoreDetail = pr.reason || (pr.ok ? \"reachable\" : \"fail\");\n } else if (persistence.mode === \"firebase\" && !persistence.db) {\n firestoreOk = false;\n firestoreDetail = \"firebase_not_initialized\";\n }\n const llmOk = rhizohLlmEnvConfigured();\n const overallOk = llmOk && firestoreOk;\n const latencyMs = Date.now() - t0;\n sendJson(res, 200, {\n ok: overallOk,\n dns: true,\n llm: llmOk,\n firestore: persistence.mode === \"firebase\" ? firestoreOk : false,\n persistence: persistence.mode,\n firestoreDetail,\n latencyMs,\n service: \"castle-gateway\",\n wsPort: PORT\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health\") {\n const persistence = getFirebasePersistence();\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n wsPort: PORT,\n persistence: persistence.mode,\n presenceMesh: \"sse\"\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/health\") {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n sendJson(res, 200, {\n ok: true,\n role: \"gateway\",\n queueLagMs: infraMetrics.queueLag,\n queueDepth: infraMetrics.queueDepth,\n errors: infraMetrics.errors,\n status: scored.status,\n score: scored.score,\n reasons: scored.reasons,\n worker: worker\n ? {\n status: worker.status,\n score: worker.score,\n reasons: worker.reasons\n }\n : null,\n timestamp: Date.now()\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisRuntime) {\n const payload = await buildGenesisRuntimeSurfacePayloadLive();\n sendJson(res, 200, payload);\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.genesisIngress) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const actor = await resolveMeshActor(req);\n const clientId = actor.ok ? actor.clientUid : (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestGenesisWorldObservationV0(body, { clientId });\n if (!result.ok) {\n const status =\n result.error === \"rate_limited\" || result.error === \"agent_spoke_throttled\"\n ? 429\n : result.error === \"payload_too_large\"\n ? 413\n : 400;\n sendJson(res, status, result);\n return;\n }\n sendJson(res, result.idempotent ? 200 : 202, result);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisStream) {\n const corsOrigin = accessControlAllowOriginValue(req);\n if (!corsOrigin && String(process.env.NODE_ENV || \"\").toLowerCase() === \"production\") {\n const og = String(req.headers?.origin || \"\").trim();\n if (og) {\n console.warn(\"[GATEWAY] genesis SSE: missing Access-Control-Allow-Origin for browser Origin (CORS whitelist mismatch):\", og);\n }\n }\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n res.write(\"retry: 3000\\n\\n\");\n res.write(\": genesis continuity stream\\n\\n\");\n res.write(\"event: genesis\\n\");\n res.write(`data: {\"booted\":true,\"pid\":${process.pid}}\\n\\n`);\n registerGenesisSseClient(res, req);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLatest) {\n const cp = getLatestGenesisSignedCheckpoint();\n if (!cp) {\n sendJson(res, 404, { ok: false, error: \"no_checkpoint_yet\", hint: \"wait_for_seq_interval_or_configure_signing_secret\" });\n return;\n }\n sendJson(res, 200, { ok: true, checkpoint: cp });\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(rhizohRuntime.routes.genesisCheckpointBySeqPrefix)) {\n const tail = pathname.slice(rhizohRuntime.routes.genesisCheckpointBySeqPrefix.length).split(\"/\")[0];\n const seq = parseInt(String(tail || \"\").trim(), 10);\n const out = await genesisCheckpointQueryBySeqV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"checkpoint_not_found\") {\n sendJson(res, 404, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointRange) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryRangeV0(from, to);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"range_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLineage) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const seq = parseInt(String(u.searchParams.get(\"seq\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryLineageV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"lineage_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisContinuityEvents) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const limit = parseInt(String(u.searchParams.get(\"limit\") || \"\").trim(), 10);\n const out = await queryGenesisContinuityEventArchiveV0(from, to, type, limit);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\" || out.error === \"event_archive_disabled\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"archive_file_too_large\") {\n sendJson(res, 413, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplay) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await resolveGenesisReplayRouterV1({\n from: fromSeq,\n to: toSeq,\n type,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayDiff) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const out = await computeGenesisReplayTemporalDiffV1({ from: fromSeq, to: toSeq, type });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEquivalence) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from1 = parseInt(String(u.searchParams.get(\"from1\") || \"\").trim(), 10);\n const to1 = parseInt(String(u.searchParams.get(\"to1\") || \"\").trim(), 10);\n const from2 = parseInt(String(u.searchParams.get(\"from2\") || \"\").trim(), 10);\n const to2 = parseInt(String(u.searchParams.get(\"to2\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await compareGenesisReplayEquivalenceV1({\n from1,\n to1,\n from2,\n to2,\n type,\n includeCheckpoints\n });\n if (\n !out.ok &&\n (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")\n ) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayAnalytics) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayAnalyticsV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEvolution) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const collapseWindows = parseInt(String(u.searchParams.get(\"collapseWindows\") || \"0\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayEvolutionV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n collapseWindows,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/metrics\") {\n res.writeHead(200, { \"Content-Type\": \"text/plain; version=0.0.4; charset=utf-8\" });\n res.end(renderInfraMetricsProm());\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.infraPrometheusMetrics) {\n if (process.env.CASTLE_PROMETHEUS_METRICS !== \"1\") {\n sendJson(res, 404, { ok: false, error: \"prometheus_metrics_disabled\" });\n return;\n }\n res.writeHead(200, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(renderRhizohPrometheusMetrics());\n return;\n }\n if (req.method === \"GET\" && pathname.startsWith(\"/infra/traces/\")) {\n const traceId = decodeURIComponent(pathname.slice(\"/infra/traces/\".length));\n const item = await getTraceById(traceId);\n if (!item) return sendJson(res, 404, { ok: false, error: \"trace_not_found\" });\n return sendJson(res, 200, { ok: true, trace: item });\n }\n if (req.method === \"GET\" && pathname === \"/infra/traces\") {\n const u = new URL(String(req.url || \"/infra/traces\"), `http://${req.headers.host || \"localhost\"}`);\n const sessionId = String(u.searchParams.get(\"sessionId\") || \"\").trim();\n const items = sessionId ? await listTracesBySession(sessionId) : [];\n return sendJson(res, 200, { ok: true, sessionId, traces: items });\n }\n\n /** Presence room mesh (C): JOIN / LEAVE / SNAPSHOT / DELTA / REPLAY (HTTP); SUBSCRIBE = SSE stream. */\n if (req.method === \"POST\" && pathname === \"/presence/mesh/join\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid, clientUid: actor.clientUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/leave\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshLeave(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/snapshot\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n sendJson(res, 200, meshSnapshot(roomUid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/delta\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, MAX_MESSAGE_BYTES);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const seq = meshAppendDelta(roomUid, actor.clientUid, {\n node: body?.node,\n projectionPatch: body?.projectionPatch,\n writerSubject: body?.writerSubject\n });\n sendJson(res, 200, { ok: true, roomUid, seq });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/replay\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const fromNodeId = body?.fromNodeId != null ? String(body.fromNodeId) : \"\";\n const fromSeq = body?.fromSeq;\n if (fromNodeId) {\n sendJson(res, 200, meshReplayFromNodeId(roomUid, fromNodeId));\n return;\n }\n sendJson(res, 200, meshReplayFromSeq(roomUid, fromSeq));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/presence/mesh/subscribe\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const roomUid = String(q.searchParams.get(\"roomUid\") || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n const write = (chunk) => {\n try {\n res.write(chunk);\n } catch {\n /* closed */\n }\n };\n const unsub = meshSubscribeSse(roomUid, write);\n const onClose = () => {\n unsub();\n meshLeave(roomUid, actor.clientUid);\n try {\n res.end();\n } catch {\n /* ignore */\n }\n };\n req.on(\"close\", onClose);\n req.on(\"aborted\", onClose);\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, uid: auth.uid, mode: auth.mode, items: await listConnections(auth.uid) });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const id = await createConnection(auth.uid, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"create_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n await updateConnection(auth.uid, id, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"update_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"DELETE\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n await deleteConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"delete_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections/test\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n if (!checkHttpRateLimit(`llm_conn_test:${auth.uid}`, RL_LLM_CONN_TEST_PER_MIN, 60_000)) {\n return sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n }\n const hasBodyKey = !!String(payload?.apiKey || \"\").trim();\n const result = await queryRhizohLlm(\n {\n message: String(payload?.message || \"Ping from Castle Gateway\"),\n context: payload?.context || { source: \"connection_test\" },\n provider: payload?.provider,\n model: payload?.model,\n apiKey: payload?.apiKey || \"\"\n },\n { allowExternalApiKey: true, llmKeySource: hasBodyKey ? \"auto\" : \"env\" }\n );\n logLlmAccess({\n route: \"/llm/connections/test\",\n uid: auth.uid,\n llmKeyBillingOwner: result.llmKeyBillingOwner,\n llmKeyOrigin: result.llmKeyOrigin,\n provider: result.provider,\n model: result.model,\n connectionId: null\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"connection_test_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAgentIdentities(auth.uid);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerAgentIdentity({\n ownerUid: auth.uid,\n agentId: payload?.agentId,\n role: payload?.role,\n personaSeed: payload?.personaSeed || {},\n capabilityLevel: payload?.capabilityLevel\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/agents/identities/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const owns = await getAgentIdentity(id);\n if (!owns || owns.ownerUid !== auth.uid) return sendJson(res, 403, { ok: false, error: \"forbidden\" });\n const payload = await readHttpJson(req);\n const row = await updateAgentIdentity(id, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/academy/events\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/academy/events\"}`);\n const status = String(url.searchParams.get(\"status\") || \"\");\n const items = await listAcademyEvents({ ownerUid: auth.uid, status, limit: 120 });\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/academy/events\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const ev = await queueAcademyEvent({\n type: payload?.type,\n roomId: payload?.roomId,\n ownerUid: auth.uid,\n participants: payload?.participants || [],\n topic: payload?.topic || \"academy\",\n payload: payload?.payload || {}\n });\n sendJson(res, 200, { ok: true, event: ev });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && String(req.url || \"\").startsWith(\"/academy/events/\") && String(req.url || \"\").endsWith(\"/resolve\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const parts = String(req.url || \"\").split(\"/\").filter(Boolean);\n const eventId = parts[2] || \"\";\n try {\n const payload = await readHttpJson(req);\n const out = await resolveAcademyEvent(eventId, payload || {});\n sendJson(res, 200, { ok: true, event: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_resolve_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/memory\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/memory\"}`);\n const scope = url.searchParams.get(\"scope\") === \"agents\" ? \"agents\" : \"users\";\n const id = String(url.searchParams.get(\"id\") || (scope === \"agents\" ? \"\" : auth.uid));\n const kind = String(url.searchParams.get(\"kind\") || \"\");\n const limit = Number(url.searchParams.get(\"limit\") || 40);\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n sendJson(res, 200, { ok: true, scope, id, kind, items: await listMemories({ scope, id, kind, limit }) });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const profile = await getPersonaGoalMemory(auth.uid);\n sendJson(res, 200, { ok: true, profile });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const profile = await setPersonaGoalMemory({\n uid: auth.uid,\n personaPatch: payload?.personaPatch || {},\n goals: Array.isArray(payload?.goals) ? payload.goals : null,\n preferencesPatch: payload?.preferencesPatch || {}\n });\n sendJson(res, 200, { ok: true, profile });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"profile_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/compact\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const out = await autoCompactMemories({ scope, id });\n sendJson(res, 200, { ok: true, scope, id, ...out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_compact_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/context\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const context = await getMemoryContext({\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n query: String(payload?.query || \"\"),\n limit: Number(payload?.limit || 80)\n });\n sendJson(res, 200, { ok: true, context });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_context_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/place-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const placeName = String(payload?.placeName || \"\").trim();\n if (!placeName) return sendJson(res, 400, { ok: false, error: \"placeName_required\" });\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const persona = String(payload?.waypointPersona || payload?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(placeName)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Place: ${placeName}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Give a concise conversational briefing about this place for a walking-route companion.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-place-brief\", placeName, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `PLACE_BRIEF:${placeName} => ${String(summary.reply || \"\").slice(0, 900)}`,\n tags: [\"place\", \"brief\", \"event-layer\"],\n importance: 0.62,\n meta: { placeName }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"place-brief\",\n text: `${placeName} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: {\n placeName,\n directive: summary.directive,\n lat: hit?.lat ? Number(hit.lat) : null,\n lon: hit?.lon ? Number(hit.lon) : null\n }\n });\n sendJson(res, 200, { ok: true, place: hit || null, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"place_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/route-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const waypoints = Array.isArray(payload?.waypoints) ? payload.waypoints.slice(0, 24) : [];\n const out = [];\n for (const wp of waypoints) {\n const name = String(wp?.name || \"\").trim();\n if (!name) continue;\n const persona = String(wp?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(name)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Waypoint: ${name}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Generate a concise spoken briefing for route waypoint.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-route-brief\", waypoint: name, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n out.push({\n name,\n persona,\n place: hit || null,\n reply: summary.reply,\n directive: summary.directive\n });\n }\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `ROUTE_BRIEF: waypoints=${out.length} ${out.map((x) => `${x.name}:${x.persona}`).join(\" | \")}`.slice(0, 1300),\n tags: [\"route\", \"waypoint\", \"brief\", \"event-layer\"],\n importance: 0.74,\n meta: { count: out.length }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"route-brief\",\n text: out.map((x) => `${x.name}:${String(x.reply || \"\").slice(0, 140)}`).join(\" | \").slice(0, 1400),\n roomId: \"studio-main\",\n meta: {\n waypointCount: out.length,\n waypoints: out.map((x) => ({\n name: x.name,\n persona: x.persona,\n lat: x.place?.lat ? Number(x.place.lat) : null,\n lon: x.place?.lon ? Number(x.place.lon) : null\n }))\n }\n });\n sendJson(res, 200, { ok: true, items: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"route_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/pdf-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const title = String(payload?.title || \"document\");\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const providedText = String(payload?.text || \"\");\n const url = String(payload?.url || \"\");\n let content = providedText;\n if (!content && url) content = await fetchTextFromUrl(url);\n if (!content) return sendJson(res, 400, { ok: false, error: \"text_or_supported_url_required\" });\n const summary = await queryRhizohLlm(\n {\n message: `Summarize this document and provide key points + practical actions.\\nTitle:${title}\\nContent:\\n${content.slice(0, 12000)}`,\n context: { source: \"event-layer-pdf-brief\", title },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `DOC_BRIEF:${title} => ${String(summary.reply || \"\").slice(0, 1000)}`,\n tags: [\"document\", \"brief\", \"library\", \"event-layer\"],\n importance: 0.7,\n meta: { title, url: url || null }\n });\n await addTranscript(auth.uid, {\n source: \"library-brief\",\n eventType: \"pdf-brief\",\n text: `${title} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { title }\n });\n sendJson(res, 200, { ok: true, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/pdf/upload\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req, 8 * 1024 * 1024);\n const job = await enqueuePdfJob({\n uid: auth.uid,\n fileName: payload?.fileName || \"document.pdf\",\n bytesBase64: payload?.contentBase64\n });\n sendJson(res, 200, { ok: true, job });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_upload_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/session\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createPublishSession(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session\",\n text: `Publish session armed: ${row.protocol} ${row.target}`.slice(0, 500),\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: row.id, bridge: row.bridge }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/studio/publish/sessions\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listPublishSessions(auth.uid, 80);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/health-check\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const protocol = String(payload?.protocol || \"WHIP\");\n const target = String(payload?.target || \"\");\n const health = await checkPublishTargetHealth(protocol, target);\n if (payload?.sessionId) {\n await updatePublishSession(auth.uid, String(payload.sessionId), {\n status: health.ok ? \"healthy\" : \"degraded\",\n reconnectPolicy: {\n enabled: true,\n backoffSec: [2, 5, 10, 20],\n nextRetryInSec: health.reconnectInSec || 0,\n maxRetries: 8,\n reason: health.reason\n }\n });\n }\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-health\",\n text: `${protocol} ${target} => ${health.ok ? \"healthy\" : \"degraded\"} (${health.reason})`,\n roomId: \"studio-main\",\n meta: { protocol, target, reconnect: !!health.shouldReconnect }\n });\n sendJson(res, 200, { ok: true, health });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_health_check_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/studio/publish/sessions/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updatePublishSession(auth.uid, id, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session-update\",\n text: `Session ${id} -> ${row.status || \"updated\"}`,\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: id, status: row.status, ingestUrl: row.ingestUrl || \"\" }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/transcripts\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/transcripts\"}`);\n const limit = Math.max(1, Math.min(200, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await listTranscripts(auth.uid, limit);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/transcripts\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await addTranscript(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"transcript_write_failed\" });\n }\n return;\n }\n\n /** E1: broadcast intent → GreenRoom capability → transcript + memory artifact */\n if (req.method === \"POST\" && req.url === \"/studio/capabilities/greenroom\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const traceId = String(payload?.traceId || `TRC-${Date.now().toString(36).toUpperCase()}`);\n const title = String(payload?.title || \"Castle Live Broadcast\").slice(0, 200);\n const intentRaw = String(payload?.intentRaw || \"\").slice(0, 800);\n const audienceEstimate = Math.max(0, Math.min(50_000_000, Number(payload?.audienceEstimate) || 1200));\n const roomId = String(payload?.roomId || \"greenroom-main\");\n const transcriptRow = await addTranscript(auth.uid, {\n source: \"rhizoh\",\n eventType: \"BROADCAST_ROUTED\",\n text: `GreenRoom · ${title}${intentRaw ? `\\nIntent: ${intentRaw}` : \"\"}`,\n roomId,\n meta: {\n ack: \"BROADCAST_ROUTED\",\n capability: \"greenroom\",\n intentType: \"broadcast\",\n traceId,\n audienceEstimate,\n artifact: {\n kind: \"broadcast_card\",\n title,\n traceId,\n roomId,\n createdAt: Date.now()\n }\n }\n });\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n text: `GREENROOM_BROADCAST:${title} trace=${traceId}`,\n tags: [\"broadcast\", \"greenroom\", \"artifact\", \"castle-library\"],\n importance: 0.72,\n kind: \"episodic\",\n meta: { traceId, capability: \"greenroom\", transcriptId: transcriptRow.id }\n });\n sendJson(res, 200, {\n ok: true,\n ack: \"BROADCAST_ROUTED\",\n traceId,\n transcript: transcriptRow,\n replayPath: `/replay/${encodeURIComponent(traceId)}`,\n sharePath: `/replay/${encodeURIComponent(traceId)}`\n });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"greenroom_capability_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/metadata/search\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/metadata/search\"}`);\n const q = String(url.searchParams.get(\"q\") || \"\");\n const limit = Math.max(1, Math.min(300, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await searchMetadata(auth.uid, q, limit);\n sendJson(res, 200, { ok: true, q, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerDevice(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, device: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"device_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const lat = Number(payload?.lat);\n const lon = Number(payload?.lon);\n const speed = Math.max(0, Number(payload?.speed || 0));\n const cfgCenterLat = Number(process.env.CASTLE_GEOFENCE_CENTER_LAT || 41.0082);\n const cfgCenterLon = Number(process.env.CASTLE_GEOFENCE_CENTER_LON || 28.9784);\n const cfgRadiusM = Math.max(100, Number(process.env.CASTLE_GEOFENCE_RADIUS_M || 55000));\n const cfgSpeedCap = Math.max(1, Number(process.env.CASTLE_SPEED_CAP_MPS || 35));\n const dist = Number.isFinite(lat) && Number.isFinite(lon) ? haversineMeters(cfgCenterLat, cfgCenterLon, lat, lon) : 0;\n const geofenceOk = !Number.isFinite(dist) ? true : dist <= cfgRadiusM;\n const speedOk = speed <= cfgSpeedCap;\n const failsafe = !(geofenceOk && speedOk);\n const safety = {\n geofenceOk,\n speedOk,\n failsafe,\n distanceFromCenterM: Number.isFinite(dist) ? dist : null,\n speedCapMps: cfgSpeedCap\n };\n const row = await appendTelemetry(auth.uid, deviceId, {\n lat,\n lon,\n speed,\n battery: payload?.battery,\n mode: payload?.mode || (failsafe ? \"FAILSAFE_HOLD\" : \"ACTIVE\"),\n raw: payload?.raw || {}\n });\n await updateDevice(auth.uid, deviceId, {\n status: failsafe ? \"failsafe\" : \"active\",\n lastTelemetryAt: row.ts,\n safetyState: safety\n });\n if (failsafe) {\n await addTranscript(auth.uid, {\n source: \"robotics-guardrails\",\n eventType: \"failsafe\",\n text: `Device ${deviceId} failsafe triggered geofenceOk=${geofenceOk} speedOk=${speedOk}`,\n roomId: \"studio-main\",\n meta: { deviceId, safety }\n });\n }\n sendJson(res, 200, { ok: true, telemetry: row, safety, command: failsafe ? \"FAILSAFE_HOLD\" : \"CONTINUE\" });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"telemetry_ingest_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listTelemetry(auth.uid, 150);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createAutomation(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"automation-scheduled\",\n text: `Automation scheduled: ${row.title} at ${new Date(row.scheduleAt).toLocaleString()}`,\n roomId: \"studio-main\",\n meta: { automationId: row.id, action: row.action, deviceId: row.deviceId }\n });\n sendJson(res, 200, { ok: true, automation: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"automation_create_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAutomations(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listDevices(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const devs = await listDevices(auth.uid, 500);\n const d = devs.find((x) => x.id === deviceId);\n if (!d) return sendJson(res, 404, { ok: false, error: \"device_not_found\" });\n const action = String(payload?.action || \"\");\n const caps = Array.isArray(d.capabilityProfile) ? d.capabilityProfile : [];\n if (action && caps.length && !caps.includes(action) && !caps.includes(\"*\")) {\n return sendJson(res, 400, { ok: false, error: \"unsupported_action_for_device\", capabilityProfile: caps });\n }\n const cmd = await enqueueCommand(auth.uid, {\n deviceId,\n action: action || \"NOOP\",\n params: payload?.params || {},\n adapter: payload?.adapter || d.adapter || \"websocket\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-command-queue\",\n eventType: \"command-queued\",\n text: `${cmd.deviceId} -> ${cmd.action}`,\n roomId: \"studio-main\",\n meta: { commandId: cmd.id, adapter: cmd.adapter }\n });\n sendJson(res, 200, { ok: true, command: cmd });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listCommands(auth.uid, 200);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/robotics/commands/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updateCommandStatus(auth.uid, id, payload?.status || \"sent\");\n sendJson(res, 200, { ok: true, command: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/rhizoh/bridge\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const result = await runRhizohBrainV2({\n message: String(payload?.message || \"robotics bridge command\"),\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n worldState: payload?.worldState || {},\n deviceState: payload?.deviceState || {},\n intent: \"robotics-mechanics\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-bridge\",\n eventType: \"robotics-command\",\n text: `Bridge -> ${String(result.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { directive: result.directive, agentId: payload?.agentId || \"\" }\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"robotics_bridge_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/planner/graph\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const graph = buildTaskActionGraph(String(payload?.task || \"\"), payload?.context || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"planner-graph\",\n text: `Planner graph built: ${graph.taskTitle} nodes=${graph.nodes.length}`,\n roomId: \"studio-main\",\n meta: { taskTitle: graph.taskTitle, nodeCount: graph.nodes.length }\n });\n sendJson(res, 200, { ok: true, graph });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"planner_graph_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerSocialChannel(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, channel: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_channel_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listSocialChannels(auth.uid, 100);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/broadcast\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const text = String(payload?.text || \"\").slice(0, 1200);\n const channels = await listSocialChannels(auth.uid, 100);\n const enabled = channels.filter((c) => c.status === \"enabled\");\n const deliveries = [];\n for (const c of enabled) {\n const out = await deliverSocialWithRetry(c, text);\n deliveries.push({ id: c.id, platform: c.platform, ...out });\n }\n const okCount = deliveries.filter((d) => d.ok).length;\n await addTranscript(auth.uid, {\n source: \"social-layer\",\n eventType: \"social-broadcast\",\n text: `Broadcast delivered ${okCount}/${enabled.length} channel(s): ${text.slice(0, 240)}`,\n roomId: \"studio-main\",\n meta: { deliveries }\n });\n sendJson(res, 200, { ok: true, sentChannels: okCount, totalChannels: enabled.length, deliveries });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_broadcast_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/ethics/programs\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, ...buildEthicsPrograms() });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands/dispatch\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const all = await listCommands(auth.uid, 200);\n const queued = all.filter((c) => String(c.status || \"queued\") === \"queued\").slice(0, 50);\n const devices = await listDevices(auth.uid, 500);\n const byId = new Map(devices.map((d) => [d.id, d]));\n const results = [];\n for (const cmd of queued) {\n const dev = byId.get(cmd.deviceId);\n if (!dev) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: \"device_not_found\" });\n continue;\n }\n try {\n const dispatched = await dispatchCommandToAdapter(dev, cmd);\n await updateCommandStatus(auth.uid, cmd.id, \"sent\");\n results.push({ id: cmd.id, ok: true, adapter: dispatched.adapter, via: dispatched.via });\n } catch (error) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: String(error?.message || \"dispatch_failed\") });\n }\n }\n await addTranscript(auth.uid, {\n source: \"robotics-command-dispatch\",\n eventType: \"command-dispatch-batch\",\n text: `Dispatch batch size=${results.length} ok=${results.filter((x) => x.ok).length}`,\n roomId: \"studio-main\",\n meta: { results: results.slice(0, 30) }\n });\n sendJson(res, 200, { ok: true, processed: results.length, results });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"dispatch_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/pdf/jobs/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n const job = await getPdfJob(auth.uid, id);\n if (!job) return sendJson(res, 404, { ok: false, error: \"pdf_job_not_found\" });\n sendJson(res, 200, { ok: true, job });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const row = await appendMemory({\n scope,\n id,\n text: payload?.text,\n tags: payload?.tags || [],\n importance: payload?.importance,\n kind: payload?.kind || \"episodic\",\n meta: { uid: auth.uid, ...(payload?.meta || {}) }\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.externalTruth) {\n try {\n if (process.env.RHIZOH_EXTERNAL_TRUTH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_truth:${ip}`, RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n sendJson(res, 200, buildRhizohExternalGroundTruthPayload(req));\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.productOutcome) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const rawBody = await readHttpBodyText(req, 64 * 1024);\n const body = rawBody.trim() ? safeJsonParse(rawBody) : null;\n if (!body || typeof body !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"invalid_json_payload\" });\n return;\n }\n const sigHdr = String(req.headers?.[\"x-rhizoh-outcome-signature\"] || \"\").trim();\n const hmacV = verifyRhizohOutcomeHmac(rawBody, sigHdr);\n if (!hmacV.ok) {\n sendJson(res, 401, { ok: false, error: hmacV.error || \"outcome_hmac_invalid\" });\n return;\n }\n const srcTokHdr = String(req.headers?.[\"x-rhizoh-outcome-source-token\"] || \"\").trim();\n const srcV = verifyRhizohOutcomeSourceToken(body.source, srcTokHdr);\n if (!srcV.ok) {\n sendJson(res, 401, { ok: false, error: srcV.error || \"outcome_source_token_invalid\" });\n return;\n }\n const subjectKey = String(body.subjectKey || \"\").trim().slice(0, 128);\n if (\n subjectKey &&\n !checkHttpRateLimit(\n `rhizoh_product_outcome_subj:${subjectKey}`,\n RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN,\n 60_000\n )\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_subject\" });\n return;\n }\n const result = ingestRhizohProductOutcomeHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n schemaVersion: \"1.1.0\",\n aggregate: result.aggregate,\n integrity: result.integrity\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"outcome_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.productOutcomeAggregate) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome_agg:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const u = new URL(String(req.url || \"/\"), \"http://localhost\");\n const cohortId = u.searchParams.get(\"cohortId\") || \"\";\n const decisionFingerprint = u.searchParams.get(\"decisionFingerprint\") || \"\";\n const limit = u.searchParams.get(\"limit\") || \"50\";\n const rows = listRhizohProductOutcomeAggregates({\n cohortId: cohortId || undefined,\n decisionFingerprint: decisionFingerprint || undefined,\n limit: Number(limit) || 50\n });\n sendJson(res, 200, { ok: true, schemaVersion: \"1.1.0\", rows });\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.externalLossBatch) {\n try {\n if (process.env.RHIZOH_EXTERNAL_LOSS_BATCH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_loss_batch:${ip}`, RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const result = ingestRhizohExternalLossBatchHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, { ok: true, accepted: result.accepted ?? 0 });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"batch_failed\") });\n }\n return;\n }\n\n function moderationAdminOk(req) {\n const expected = String(process.env.CASTLE_MODERATION_ADMIN_KEY || \"\").trim();\n if (!expected) return false;\n const got = String(req.headers[\"x-castle-moderation-key\"] || \"\").trim();\n return got.length > 0 && got === expected;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.opsAbuseReport) {\n try {\n const body = await readHttpJson(req, 32 * 1024);\n const auth = await resolveHttpUser(req);\n const entry = submitAbuseReportV0({\n reporterUid: auth.ok ? auth.uid : null,\n targetUid: body?.targetUid ?? null,\n category: body?.category,\n detail: body?.detail,\n traceId: body?.traceId ?? null\n });\n sendJson(res, 202, { ok: true, reportId: entry.id, status: entry.status });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsModerationQueue) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n queue: listModerationQueueV0(100),\n softBlocked: listSoftBlockedUsersV0()\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-block\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const out = softBlockUserV0(body?.uid, body?.reason);\n sendJson(res, out.ok ? 200 : 400, out);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-unblock\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n sendJson(res, 200, removeSoftBlockV0(body?.uid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsAgentSnapshots) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n const limit = Number(new URL(req.url || \"\", \"http://local\").searchParams.get(\"limit\") || 50);\n sendJson(res, 200, { ok: true, snapshots: listRecentAgentSnapshotsV0(limit) });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.academicObservatoryExport) {\n const auth = await resolveHttpUser(req);\n const out = handleAcademicObservatoryExportGetV0(req, auth);\n sendJson(res, out.status, out.body);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsHardeningStatus) {\n const auth = await resolveHttpUser(req);\n const principal = auth.ok ? `uid:${auth.uid}` : null;\n const gclSnap = await getGlobalCostLedgerSnapshotV0(principal);\n const economicStrategy = evaluateEconomicStrategyL3V0(gclSnap);\n const hardeningTenantId = resolveHardeningTenantIdV0(auth, req);\n const unifiedState = await buildUnifiedStateNarrativeV0({\n tenantId: hardeningTenantId ?? undefined,\n platformScope: !hardeningTenantId,\n principal\n });\n sendJson(res, 200, {\n ok: true,\n schema: \"rhizoh.ops.hardening_status.v0\",\n containment: readAgentContainmentConfigV0(),\n cost: readCostContainmentConfigV0(),\n gcl: {\n backend: await getGclBackendStatusV0(),\n snapshot: gclSnap\n },\n economicStrategyL3: economicStrategy,\n phasedRollout: await getPhasedRolloutStatsV0(),\n tier: readPhasedRolloutTierV0(),\n principalCost: auth.ok ? await getCostContainmentStatsV0(principal) : null,\n stressTaxonomy: verifyStressTaxonomyCoverageV0(),\n stressConflictResolution: verifyStressConflictResolutionV0(),\n resolutionStabilityEnvelope: verifyResolutionStabilityEnvelopeV0(),\n unifiedState: {\n tenantScope: {\n tenantId: unifiedState.tenantScope?.tenantId,\n scope: unifiedState.tenantScope?.scope,\n noGlobalDerivedState: unifiedState.tenantScope?.noGlobalDerivedState\n },\n narrativeFingerprint: unifiedState.narrativeFingerprint,\n screenshotScopeWatermark: unifiedState.screenshotScopeWatermark,\n interpretationSafetyContract: unifiedState.interpretationSafetyContract,\n interpretationUxContract: {\n schema: unifiedState.interpretationUxContract?.schema,\n layerOrder: unifiedState.interpretationUxContract?.layerOrder,\n narrativeNeverVisuallyDominant: unifiedState.interpretationUxContract?.narrativeNeverVisuallyDominant\n },\n governance: {\n narrativeIsNotDecisionLayer: unifiedState.governance.narrativeIsNotDecisionLayer,\n displayOrder: unifiedState.governance.displayOrder,\n trustPosture: unifiedState.governance.trustPosture,\n interpretationUxContract: unifiedState.governance.interpretationUxContract?.schema\n },\n humanOps: {\n decisionTier: unifiedState.humanOps?.humanDecisionScaling?.tier,\n decisionOwner: unifiedState.humanOps?.humanDecisionScaling?.decisionOwner,\n opsRouteId: unifiedState.humanOps?.humanDecisionOpsRunbook?.routing?.routeId,\n slaAckMinutes: unifiedState.humanOps?.humanDecisionOpsRunbook?.sla?.ackMinutes,\n uiDisclaimerRequired: unifiedState.humanOps?.narrativeUiSafety?.displayRules?.requireDisclaimerBanner,\n misreadHighResidual: unifiedState.humanOps?.misreadSimulation?.highResidualCount,\n propagationHighResidual: unifiedState.humanOps?.socialPropagationSimulation?.highResidualCount,\n worstWatermarkSurvivability:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.worstWatermarkSurvivability,\n dominantDistortionSource:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.dominantDistortionSource\n },\n culturalRisk: {\n perceptualRiskClass: unifiedState.culturalRisk?.perceptualRiskClass,\n trustFork: unifiedState.culturalRisk?.trustDynamics?.fork,\n trustDecayScore: unifiedState.culturalRisk?.trustDynamics?.scores?.trustDecay,\n mythologyScore: unifiedState.culturalRisk?.trustDynamics?.scores?.mythology,\n confidenceDetachmentExposure:\n unifiedState.culturalRisk?.trustDynamics?.axes?.confidenceDetachmentExposure\n },\n appliedSystemsLayer: {\n schema: unifiedState.appliedSystemsLayer?.schema,\n researchOutputType: unifiedState.appliedSystemsLayer?.researchGovernance?.outputType,\n roboticsBlockActuation: unifiedState.appliedSystemsLayer?.roboticsGrounding?.blockActuation,\n spiralRumorCount:\n unifiedState.appliedSystemsLayer?.spiralMMOGameKernel?.gameLayer?.rumors?.length\n },\n actionSemanticGovernance: {\n schema: unifiedState.actionSemanticGovernance?.schema,\n highRiskActionCount: unifiedState.actionSemanticGovernance?.highRiskActionCount,\n loopClosed: unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.loopClosed,\n authorityCollapseMitigated:\n unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.risks?.authorityCollapse\n ?.mitigated\n },\n actionContextResolution: {\n schema: unifiedState.actionContextResolution?.schema,\n contextFingerprint: unifiedState.actionContextResolution?.contextFingerprint?.fingerprintHash,\n domainCollision: unifiedState.actionContextResolution?.domainCollision?.detected,\n deployAgentSpiralBlocked:\n unifiedState.actionContextResolution?.actionOverloadingRisk?.deployAgentExample\n ?.spiral_mmo === \"blocked\",\n roboticsActuationEligible:\n unifiedState.actionContextResolution?.roboticsActuationCorrectness?.actuationEligible\n },\n epistemicCoherence: {\n schema: unifiedState.epistemicCoherence?.schema,\n verdict: unifiedState.epistemicCoherence?.systemCoherence?.verdict,\n coherenceScore: unifiedState.epistemicCoherence?.systemCoherence?.score,\n contradictionCount: unifiedState.epistemicCoherence?.crossLayerContradictions?.count,\n operatorDecision: unifiedState.epistemicCoherence?.uxCompression?.operatorDecision,\n headlineTr: unifiedState.epistemicCoherence?.uxCompression?.headline?.tr,\n falseSafetyWarning: Boolean(\n unifiedState.epistemicCoherence?.uxCompression?.falseSafetyWarning\n )\n },\n coherenceAuthorityBoundary: {\n schema: unifiedState.coherenceAuthorityBoundary?.schema,\n bindingValid: unifiedState.coherenceAuthorityBoundary?.eclAuthorityBinding?.valid,\n canExecute: unifiedState.coherenceAuthorityBoundary?.contract?.can_execute,\n mandatoryBannerTr:\n unifiedState.coherenceAuthorityBoundary?.disclaimers?.mandatoryBanner?.tr\n },\n decisionLatencyGovernance: {\n schema: unifiedState.decisionLatencyGovernance?.schema,\n latencyTier: unifiedState.decisionLatencyGovernance?.latencyTier,\n inflationScore: unifiedState.decisionLatencyGovernance?.latencyInflation?.score,\n fastPathEligible: unifiedState.decisionLatencyGovernance?.fastPath?.eligible,\n primaryActionId: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.primaryActionId,\n ackSlaMinutes: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.ackSlaMinutes,\n certaintyCap: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.certaintyCap,\n uncertaintyItems:\n unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.uncertaintyEnvelope?.itemCount\n },\n decisionPacketUncertaintyBoundary: {\n schema: unifiedState.decisionPacketUncertaintyBoundary?.schema,\n invariantValid: unifiedState.decisionPacketUncertaintyBoundary?.invariantCheck?.valid,\n centralInvariant: unifiedState.decisionPacketUncertaintyBoundary?.centralInvariant\n },\n epistemicDecisionPacing: {\n schema: unifiedState.epistemicDecisionPacing?.schema,\n queueSaturationTier:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation?.tier,\n operatorProcessingLatency:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation\n ?.operatorProcessingLatency,\n postureWindowCount:\n unifiedState.epistemicDecisionPacing?.temporaryStaticPostureWindows\n ?.temporaryStaticPostureWindows?.windowCount,\n executionBoundariesValid:\n unifiedState.epistemicDecisionPacing?.executionBoundaries?.valid,\n grantsExecutionApproval: unifiedState.epistemicDecisionPacing?.grantsExecutionApproval\n },\n epistemicTemporalCoherence: {\n schema: unifiedState.epistemicTemporalCoherence?.schema,\n allInvariantsValid: unifiedState.epistemicTemporalCoherence?.allInvariantsValid,\n temporalAlignmentValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.temporalAlignment?.valid,\n crossWindowGuardValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard?.valid,\n delayedTruthValid: unifiedState.epistemicTemporalCoherence?.invariants?.delayedTruthRule?.valid,\n reconciliationRequired:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard\n ?.reconciliation?.required\n },\n realityDriftObserver: {\n schema: unifiedState.realityDriftObserver?.schema,\n driftActive: unifiedState.realityDriftObserver?.driftObserverSummary?.active,\n misapprehensionShapeCount:\n unifiedState.realityDriftObserver?.misapprehensionShapeCatalog?.shapeCount,\n criticalShapeCount:\n unifiedState.realityDriftObserver?.driftObserverSummary?.criticalShapeCount,\n roboticsMismatchActive:\n unifiedState.realityDriftObserver?.roboticsFeedbackMismatch?.active\n },\n driftCausality: {\n schema: unifiedState.driftCausality?.schema,\n shapesWithCausalExplanation: unifiedState.driftCausality?.rdolLinkage?.shapesWithCausalExplanation,\n activeDomainCount: unifiedState.driftCausality?.causalitySummary?.activeDomainCount,\n propagationCausalityActive: unifiedState.driftCausality?.causalDomains?.propagation?.active\n },\n productReadiness: unifiedState.productReadiness,\n stateLayers: {\n raw: unifiedState.stateLayers.raw,\n derived: {\n systemState: unifiedState.stateLayers.derived.systemState,\n narrative: unifiedState.stateLayers.derived.narrative,\n validation: unifiedState.stateLayers.derived.validation\n },\n policy: {\n binding: unifiedState.stateLayers.policy.binding,\n decisionOwner: unifiedState.stateLayers.policy.decisionOwner,\n prohibitedActions: unifiedState.stateLayers.policy.prohibitedActions\n }\n },\n systemState: unifiedState.systemState,\n interpretation: unifiedState.interpretation,\n validation: {\n trustPosture: unifiedState.validation.trustPosture,\n adjustedConfidence: unifiedState.validation.adjustedConfidence,\n agreement: unifiedState.validation.confidenceDecomposition.agreement,\n divergenceScore: unifiedState.validation.divergence.divergenceScore,\n validated: unifiedState.validation.divergence.validated,\n uncertaintyTags: unifiedState.validation.uncertainty.tags.map((t) => t.id),\n flags: unifiedState.validation.divergence.flags.map((f) => f.id)\n },\n compression: unifiedState.compression\n }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 6 * 1024 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (\n !checkHttpRateLimit(`rhizoh_voice_transcribe:${rlKey}`, RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN, 60_000)\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const voiceEnv = rhizohVoiceTranscribeEnvV3();\n if (!voiceEnv.any) {\n sendJson(res, 503, {\n ok: false,\n error: \"voice_asr_not_configured\",\n env: voiceEnv\n });\n return;\n }\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n const result = await runRhizohVoiceTranscribeV3(\n applyLanguagePropagationToVoiceBodyV1(payload, langProp)\n );\n sendJson(res, result.ok ? 200 : 422, {\n ...result,\n traceId: String(payload?.traceId || \"\"),\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n atMs: Date.now()\n });\n } catch (error) {\n sendJson(res, 500, {\n ok: false,\n error: \"voice_transcribe_failed\",\n detail: String(error?.message || error),\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(\"/rhizoh/llm/task/\")) {\n const rawTaskId = pathname.slice(\"/rhizoh/llm/task/\".length).split(\"/\")[0];\n const taskId = String(rawTaskId || \"\").toLowerCase() === \"last\" ? getLatestLlmWorkerTaskIdV0() : rawTaskId;\n const snap = taskId ? getLlmWorkerTaskSnapshotV0(taskId) : null;\n if (!snap) {\n sendJson(res, 404, {\n ok: false,\n error: \"llm_task_not_found\",\n taskId: rawTaskId,\n status: \"missing\",\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n if (snap.status === \"processing\") {\n sendJson(res, 202, {\n ok: true,\n taskId: snap.taskId,\n status: \"processing\",\n createdAt: snap.createdAt,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n sendJson(res, snap.httpStatus || 200, {\n ...(snap.body || { ok: false, error: \"llm_task_empty\", taskId: snap.taskId }),\n taskId: snap.taskId,\n status: snap.status === \"completed\" ? \"completed\" : snap.status,\n LATEST_TASK_DEBUG: getLlmWorkerDebugSnapshotV0()\n });\n return;\n }\n\n if (req.method === \"POST\" && req.url === rhizohRuntime.routes.rhizohLlm) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 128 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n logRhizohHealth(\"gateway_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: String(payload?.traceId || \"\"),\n auth: auth.ok ? \"ok\" : \"anon\",\n ip\n });\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (!checkHttpRateLimit(`rhizoh_llm:${rlKey}`, RL_RHIZOH_LLM_PER_MIN, 60_000)) {\n const rlTaxonomy = classifyStressResponseV0({ code: \"rate_limit_exceeded\" });\n return sendJson(res, 429, {\n ok: false,\n error: \"rate_limit_exceeded\",\n reply: rlTaxonomy.userMessageTr || \"İstek sınırı aşıldı. Kısa süre sonra tekrar deneyin.\",\n stressClass: rlTaxonomy.stressClass,\n responseAction: rlTaxonomy.responseAction,\n stressMatrix: rlTaxonomy.matrix,\n stressInterpretable: rlTaxonomy.interpretable,\n stressConfidence: rlTaxonomy.stressConfidence,\n actionConfidence: rlTaxonomy.actionConfidence,\n actionSoftened: rlTaxonomy.actionSoftened,\n responseActionStrict: rlTaxonomy.responseActionStrict,\n actionInterpretable: rlTaxonomy.actionInterpretable,\n conflictResolution: rlTaxonomy.conflictResolution,\n directive: \"NONE\"\n });\n }\n\n const requireExplicit = process.env.CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE === \"1\";\n const keyMode = normalizeClientLlmKeySource(payload?.llmKeySource ?? payload?.keySource);\n if (keyMode === null) {\n return sendJson(res, 400, {\n ok: false,\n error: \"invalid_llm_key_source\",\n reply: \"llmKeySource geçersiz. Kullanın: env | user_connection | auto\",\n directive: \"NONE\"\n });\n }\n if (requireExplicit && keyMode === \"auto\") {\n return sendJson(res, 400, {\n ok: false,\n error: \"llm_key_source_required\",\n reply: \"Sunucu CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE=1: istekte llmKeySource (env veya user_connection) zorunlu.\",\n directive: \"NONE\"\n });\n }\n\n const conn = auth.ok ? await resolveConnection(auth.uid, String(payload?.connectionId || \"\")) : null;\n\n if (keyMode === \"user_connection\") {\n if (!auth.ok) {\n return sendJson(res, 401, {\n ok: false,\n error: \"llm_auth_required_for_user_connection\",\n reply: \"Kayıtlı LLM bağlantısı için giriş gerekli.\",\n directive: \"NONE\"\n });\n }\n if (!conn?.apiKey) {\n return sendJson(res, 400, {\n ok: false,\n error: \"user_llm_connection_required\",\n reply: \"Geçerli bir LLM bağlantısı yok; Studio’da bağlantı ekleyin veya llmKeySource=env kullanın.\",\n directive: \"NONE\"\n });\n }\n }\n\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n let safePayload = applyLanguagePropagationToLlmPayloadV1({ ...(payload || {}) }, langProp);\n const trimOut = trimRhizohLlmGatewayPayloadV1(safePayload);\n if (trimOut.meta.trimmed) {\n logRhizohHealth(\"llm_payload_trimmed\", {\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n bytesBefore: trimOut.meta.bytesBefore,\n bytesAfter: trimOut.meta.bytesAfter\n });\n }\n safePayload = trimOut.payload;\n delete safePayload.apiKey;\n delete safePayload.llmKeySource;\n delete safePayload.keySource;\n\n let resolvedProvider = payload?.provider;\n let resolvedModel = payload?.model;\n let connApiKey = \"\";\n\n if (keyMode === \"user_connection\") {\n resolvedProvider = conn.provider;\n resolvedModel = conn.model;\n connApiKey = conn.apiKey || \"\";\n } else if (keyMode === \"env\") {\n connApiKey = \"\";\n } else {\n resolvedProvider = conn?.provider || payload?.provider;\n resolvedModel = conn?.model || payload?.model;\n connApiKey = conn?.apiKey || \"\";\n }\n\n const turnInput = {\n safePayload,\n auth,\n keyMode,\n conn,\n resolvedProvider,\n resolvedModel,\n connApiKey\n };\n const turnMeta = {\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n langEcho: buildRhizohLanguagePropagationEchoV1(langProp)\n };\n\n if (isLlmWorkerEnabledV0() && isLlmWorkerAsyncEnabledV0()) {\n const taskId = enqueueLlmWorkerTaskAsyncV0(turnInput, turnMeta);\n logRhizohHealth(\"llm_worker_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n taskId,\n traceId: String(payload?.traceId || taskId),\n keyMode,\n async: true\n });\n sendJson(res, 202, {\n ok: true,\n taskId,\n status: \"processing\",\n traceId: String(payload?.traceId || taskId),\n pollPath: `/rhizoh/llm/task/${taskId}`,\n ...turnMeta.langEcho\n });\n return;\n }\n\n const { result, traceId, turnLatencyMs, spinePhases, sampledTrace } = isLlmWorkerEnabledV0()\n ? await runLlmWorkerTaskV0(turnInput, turnMeta)\n : await rhizohGatewayTurn(turnInput);\n logRhizohHealth(\"llm_response\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId,\n turnLatencyMs: Number(turnLatencyMs || 0),\n keyMode\n });\n sendJson(res, 200, {\n ...result,\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n traceId,\n turnLatencyMs,\n ...(spinePhases ? { spinePhases } : {}),\n ...(sampledTrace !== undefined ? { sampledTrace } : {})\n });\n } catch (error) {\n const msg = String(error?.message || \"\");\n const code = error?.code || \"\";\n let status = 500;\n if (msg === \"payload_too_large\") status = 413;\n else if (msg === \"rate_limit_exceeded\") status = 429;\n else if (\n code === \"cost_hard_limit\" ||\n code === \"phased_rollout_capacity\" ||\n error?.containment === true ||\n String(code).startsWith(\"agent_\")\n ) {\n status = 429;\n } else if (code === \"user_soft_blocked\" || code === \"prompt_abuse_detected\") status = 403;\n else if ([\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(code) || msg.includes(\"missing_api_key_for_\"))\n status = 400;\n let rhizohFailureKind = \"provider_error\";\n if (msg === \"rate_limit_exceeded\" || status === 429) rhizohFailureKind = \"rate_limit\";\n else if (status === 403) rhizohFailureKind = \"policy_block\";\n else if (status === 400 || status === 413) rhizohFailureKind = \"client_config\";\n else if (msg.startsWith(\"provider_http_\")) {\n const st = Number(msg.replace(\"provider_http_\", \"\"));\n if (st === 408 || st === 504) rhizohFailureKind = \"timeout\";\n else rhizohFailureKind = \"provider_error\";\n } else if (/timeout|timed out|ETIMEDOUT/i.test(msg)) rhizohFailureKind = \"timeout\";\n\n const providerHttpMatch = msg.match(/^provider_http_(\\d{3})$/);\n const stressTaxonomy =\n error?.stressClass != null\n ? {\n stressClass: error.stressClass,\n responseAction: error.responseAction,\n stressMatrix: error.stressMatrix,\n stressInterpretable: error.stressInterpretable !== false,\n stressConfidence: error.stressConfidence,\n actionConfidence: error.actionConfidence,\n actionSoftened: error.actionSoftened,\n responseActionStrict: error.responseActionStrict,\n actionInterpretable: error.actionInterpretable,\n conflictResolution: error.conflictResolution,\n stressSecondary: error.stressSecondary,\n responseActions: error.responseActions\n }\n : (() => {\n const t = classifyStressResponseV0({\n code: code || msg,\n rhizohFailureKind,\n providerHttpStatus: providerHttpMatch\n ? Number(providerHttpMatch[1])\n : null\n });\n return {\n stressClass: t.stressClass,\n responseAction: t.responseAction,\n stressMatrix: t.matrix,\n stressInterpretable: t.interpretable,\n stressConfidence: t.stressConfidence,\n actionConfidence: t.actionConfidence,\n actionSoftened: t.actionSoftened,\n responseActionStrict: t.responseActionStrict,\n actionInterpretable: t.actionInterpretable,\n conflictResolution: t.conflictResolution,\n stressSecondary: t.stressSecondary,\n responseActions: t.responseActions\n };\n })();\n sendJson(res, status, {\n ok: false,\n rhizohFailureKind,\n ...(providerHttpMatch ? { providerHttpStatus: Number(providerHttpMatch[1]) } : {}),\n error: code || msg || \"rhizoh_llm_failed\",\n detail: msg || code || undefined,\n reply:\n error?.reply ||\n (status === 500 ? \"Rhizoh bağlantısı geçici olarak kesildi.\" : msg),\n directive: error?.directive || \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n ...stressTaxonomy\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicSeal) {\n try {\n if (!EPISTEMIC_SEAL_SECRET) {\n sendJson(res, 503, { ok: false, error: \"epistemic_seal_disabled\" });\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`epistemic_seal:${ip}`, RL_EPISTEMIC_SEAL_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const truth = body.truth_contract;\n if (!truth || typeof truth !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"truth_contract_required\" });\n return;\n }\n const sealBody = {\n truth_contract: truth,\n runtime_hash: String(body.runtime_hash || \"\"),\n model_route: {\n provider: body.model_route?.provider ?? null,\n model: body.model_route?.model ?? null\n },\n memory_digest: String(body.memory_digest || \"\"),\n world_snapshot_hash: String(body.world_snapshot_hash || \"\"),\n timestamp: Number(body.timestamp) || Date.now()\n };\n const canonical = canonicalEpistemicSealString(sealBody);\n const { hash, signature } = hashAndSignEpistemicSeal(canonical, EPISTEMIC_SEAL_SECRET);\n recordGenesisEpistemicSealIssued(hash);\n const attestation = {\n schema: EPISTEMIC_SEAL_SCHEMA,\n algorithm: \"SHA-256+HMAC-SHA256\",\n keyHint: process.env.CASTLE_EPISTEMIC_SEAL_SECRET ? \"CASTLE_EPISTEMIC_SEAL_SECRET\" : \"CASTLE_GATEWAY_TOKEN\",\n issuedAt: Date.now(),\n gateway: { service: \"castle-gateway\", listenPort: PORT }\n };\n sendJson(res, 200, {\n ok: true,\n hash,\n signature,\n attestation,\n canonicalBytes: Buffer.byteLength(canonical, \"utf8\")\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"seal_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicLogsBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`epistemic_logs:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 120) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const persisted = await persistEpistemicLedgerBatch(auth.uid, entries);\n recordGenesisEpistemicLedgerPersisted(persisted.persisted);\n // Internal cognition layer: forecast stays gateway/ledger-only (no client exposure).\n try {\n await persistEpistemicForecastBatch(auth.uid, persisted.normalized || []);\n } catch {\n /* forecast failures must not break primary ledger ingest */\n }\n sendJson(res, 200, {\n ok: true,\n persisted: persisted.persisted,\n mode: persisted.mode,\n latest: persisted.normalized?.[persisted.normalized.length - 1] || null\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"epistemic_log_ingest_failed\") });\n }\n return;\n }\n\n sendJson(res, 404, { ok: false, error: \"not_found\" });\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\nspiralWssForGenesis = wss;\nconst orchestrator = createOrchestrator();\nconst clientStats = new Map();\nlet broadcasterClientId = null;\nconst spiralState = {\n rooms: [{ id: \"room-default\", name: \"Spiral Default Arena\" }],\n activeByClient: new Map(),\n characters: new Map() // id -> {id,name,role,roomId,source}\n};\n\n/** Castle social rooms — keyed by `castleRoomKey` from {@link WS_MESSAGE.CASTLE_SOCIAL_PULSE}. */\nconst castleSocialRoomState = new Map();\nconst CASTLE_SOCIAL_MEMBER_TTL_MS = 90_000;\nconst CASTLE_SOCIAL_MAX_PULSE_BYTES = 8192;\nconst RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES = 5 * 1024 * 1024;\nconst RHIZOH_VOICE_LIVE_MAX_CHUNKS = 96;\n\n/**\n * Voice Engine v3 live media lane.\n * Client streams MediaRecorder chunks over the existing gateway WS; STOP reuses the\n * gateway ASR runner so orchestration remains the single authority downstream.\n */\nconst rhizohVoiceLiveSessions = new WeakMap();\n\nfunction appendRhizohVoiceLiveEventV0(session, type, payloadRefSeed = \"\") {\n if (!session) return null;\n session.eventSeq = (Number(session.eventSeq) || 0) + 1;\n const event = createRhizohImmutableEventV0({\n type,\n sessionId: session.sessionId,\n traceId: session.traceId,\n actorSeed: session.sessionId || session.traceId,\n eventSeq: session.eventSeq,\n payloadRef: createRhizohPayloadRefV0(payloadRefSeed || `${session.sessionId}:${type}:${session.eventSeq}`)\n });\n session.events = [...(session.events || []), event].slice(-64);\n return event;\n}\n\nfunction sendRhizohVoiceLiveEnvelopeV0(socket, type, payload) {\n try {\n if (socket?.readyState === 1) socket.send(JSON.stringify(createEnvelope(type, payload)));\n } catch {\n /* noop */\n }\n}\n\nfunction decodeRhizohVoiceLiveChunkV0(payload = {}) {\n const b64 = String(payload.audioBase64 || \"\").trim();\n if (!b64) return { ok: false, error: \"audio_base64_required\" };\n try {\n const buf = Buffer.from(b64, \"base64\");\n if (!buf.length) return { ok: false, error: \"audio_decode_empty\" };\n return { ok: true, buffer: buf };\n } catch {\n return { ok: false, error: \"audio_base64_invalid\" };\n }\n}\n\nfunction handleRhizohVoiceLiveStartV0(socket, payload = {}) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const sessionId = String(p.sessionId || `live_${Date.now().toString(36)}`).slice(0, 96);\n rhizohVoiceLiveSessions.set(socket, {\n sessionId,\n traceId: String(p.traceId || \"\").slice(0, 128),\n mimeType: String(p.mimeType || \"audio/webm\").slice(0, 80),\n languageCode: String(p.languageCode || \"tr-TR\").slice(0, 32),\n path: String(p.path || \"both\").slice(0, 24),\n chunks: [],\n chunkIndexes: [],\n totalBytes: 0,\n nextChunkIndex: 1,\n eventSeq: 0,\n events: [],\n startedAtMs: Date.now()\n });\n appendRhizohVoiceLiveEventV0(rhizohVoiceLiveSessions.get(socket), \"VOICE_LIVE_SESSION_STARTED\", `${sessionId}:live_session`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_START, {\n ok: true,\n sessionId,\n schema: \"castle.rhizoh.voice_live.gateway_lane.v0\"\n });\n}\n\nfunction handleRhizohVoiceLiveChunkV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const sequence = validateRhizohVoiceLiveChunkPayloadV0(payload, {\n expectedChunkIndex: session.nextChunkIndex\n });\n if (!sequence.ok) {\n rhizohVoiceLiveSessions.delete(socket);\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:chunk_reject`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: sequence.error,\n expectedChunkIndex: sequence.expectedChunkIndex,\n chunkIndex: sequence.chunkIndex\n });\n return;\n }\n const decoded = decodeRhizohVoiceLiveChunkV0(payload);\n if (!decoded.ok) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: decoded.error\n });\n return;\n }\n if (\n session.chunks.length >= RHIZOH_VOICE_LIVE_MAX_CHUNKS ||\n session.totalBytes + decoded.buffer.length > RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES\n ) {\n rhizohVoiceLiveSessions.delete(socket);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_audio_too_large\"\n });\n return;\n }\n session.chunks.push(decoded.buffer);\n session.chunkIndexes.push(sequence.chunkIndex);\n session.totalBytes += decoded.buffer.length;\n session.nextChunkIndex += 1;\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_CHUNK_ACCEPTED\", `${session.sessionId}:chunk:${sequence.chunkIndex}`);\n}\n\nfunction handleRhizohVoiceLiveStopV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n rhizohVoiceLiveSessions.delete(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const lastChunkIndex = Number(payload?.lastChunkIndex);\n if (Number.isInteger(lastChunkIndex) && lastChunkIndex !== session.chunks.length) {\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:stop_mismatch`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_chunk_count_mismatch\",\n expectedChunkCount: lastChunkIndex,\n receivedChunkCount: session.chunks.length\n });\n return;\n }\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_STOP_RECEIVED\", `${session.sessionId}:stop:${session.chunks.length}`);\n const audio = Buffer.concat(session.chunks);\n if (!audio.length) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"audio_empty\"\n });\n return;\n }\n runRhizohVoiceTranscribeV3({\n path: String(payload?.path || session.path || \"both\"),\n audioBase64: audio.toString(\"base64\"),\n mimeType: String(payload?.mimeType || session.mimeType || \"audio/webm\"),\n languageCode: String(payload?.languageCode || session.languageCode || \"tr-TR\"),\n traceId: String(payload?.traceId || session.traceId || \"\"),\n sessionId: session.sessionId,\n transport: \"gateway_ws_live\"\n })\n .then((result) => {\n appendRhizohVoiceLiveEventV0(session, result?.ok ? \"VOICE_TRANSCRIBE_FINAL\" : \"VOICE_TRANSCRIBE_FAILED\", `${session.sessionId}:final`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_FINAL, {\n ...result,\n sessionId: session.sessionId,\n transportPath: \"gateway_ws_live\",\n bytes: audio.length,\n chunkCount: session.chunks.length,\n chunkSequence: Object.freeze({\n ok: true,\n firstChunkIndex: session.chunkIndexes[0] || null,\n lastChunkIndex: session.chunkIndexes[session.chunkIndexes.length - 1] || null,\n receivedChunkCount: session.chunks.length\n }),\n eventTrace: Object.freeze((session.events || []).slice(-16)),\n recordedMs: Math.max(0, Date.now() - session.startedAtMs)\n });\n })\n .catch((e) => {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_transcribe_failed\",\n detail: String(e?.message || e)\n });\n });\n}\n\nfunction removeCastleSocialClientFromAllRooms(clientId) {\n const cid = String(clientId || \"\");\n if (!cid) return;\n for (const room of castleSocialRoomState.values()) {\n room.members.delete(cid);\n }\n}\n\nfunction buildCastleSocialRoomPayload(roomKey) {\n const room = castleSocialRoomState.get(roomKey);\n if (!room) return null;\n const now = Date.now();\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const roster = [...room.members.values()].map((m) => ({\n userId: m.userId,\n lastMs: m.lastMs,\n ...m.sp\n }));\n const peerFeeds =\n room.walPeers && room.walPeers.size\n ? [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }))\n : [];\n return { castleRoomKey: roomKey, seq: room.seq, roster, peerFeeds, ts: now };\n}\n\nconst CASTLE_WAL_PEER_MAX_FEED_BYTES = 48 * 1024;\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {{ ok: boolean, code?: string, reason?: string }} gate\n */\nfunction rejectWalPeerIngressV0(socket, gate) {\n const code = gate.code || \"wal_peer_rejected\";\n recordWalPeerRejectV0(code);\n recordSubstrateOtelEventV0(\"wal_peer_reject\", { code, reason: String(gate.reason || \"\").slice(0, 120) });\n socket.send(\n JSON.stringify(\n createEnvelope(WS_MESSAGE.ERROR, {\n error: gate.reason || \"WAL peer ingress rejected.\",\n code\n })\n )\n );\n}\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {Record<string, unknown> | null | undefined} walPeerFeed\n * @returns {boolean}\n */\nfunction gateWalPeerIngressV0(socket, walPeerFeed) {\n const authGate = assertWalPeerSocketAuthorityV0(socket);\n if (!authGate.ok) {\n rejectWalPeerIngressV0(socket, authGate);\n return false;\n }\n const sigGate = validateWalPeerFeedSignatureV0(\n walPeerFeed && typeof walPeerFeed === \"object\" ? walPeerFeed : {}\n );\n if (!sigGate.ok) {\n rejectWalPeerIngressV0(socket, sigGate);\n return false;\n }\n return true;\n}\n\nfunction handleCastleWalPeerFeed(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_WAL_PEER_MAX_FEED_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_WAL_PEER_FEED too large.\" })));\n return;\n }\n const walPeerFeed = p.walPeerFeed && typeof p.walPeerFeed === \"object\" ? p.walPeerFeed : {};\n if (!gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const castleRoomKey = String(p.castleRoomKey || \"wal:convergence\").slice(0, 64);\n const castleId = String(p.castleId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId,\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: room.members.get(socket.clientId)?.sp ?? {}\n });\n const peerFeeds = [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }));\n const body = {\n castleRoomKey,\n seq: room.seq,\n peerFeeds,\n ts: now\n };\n const env = createEnvelope(WS_MESSAGE.CASTLE_WAL_PEER_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys(), ...room.walPeers.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction handleCastleSocialPulse(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_SOCIAL_MAX_PULSE_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_SOCIAL_PULSE too large.\" })));\n return;\n }\n const castleRoomKey = String(p.castleRoomKey || \"default\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n const sp = p.socialPulse && typeof p.socialPulse === \"object\" ? p.socialPulse : {};\n const walPeerFeed =\n p.walPeerFeed && typeof p.walPeerFeed === \"object\"\n ? p.walPeerFeed\n : sp.walPeerFeed && typeof sp.walPeerFeed === \"object\"\n ? sp.walPeerFeed\n : null;\n if (walPeerFeed && !gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const spNext = walPeerFeed ? { ...sp, walPeerFeed } : { ...sp };\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: spNext\n });\n if (walPeerFeed) {\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId: String(p.castleId || userId).slice(0, 64),\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n }\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const body = buildCastleSocialRoomPayload(castleRoomKey);\n if (!body) return;\n const env = createEnvelope(WS_MESSAGE.CASTLE_SOCIAL_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcast(msgObj) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcastState() {\n broadcast(\n createEnvelope(WS_MESSAGE.BROADCAST_STATE, {\n broadcasterClientId,\n viewers: [...wss.clients].filter((c) => c.readyState === 1).length\n })\n );\n}\n\nfunction currentSpiralStatePayload() {\n return {\n rooms: spiralState.rooms,\n characters: [...spiralState.characters.values()]\n };\n}\n\nfunction broadcastSpiralState() {\n broadcast(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload()));\n}\n\nfunction ensureRoom(roomId, fallbackName = null) {\n if (!roomId) return spiralState.rooms[0].id;\n const exists = spiralState.rooms.find((r) => r.id === roomId);\n if (exists) return exists.id;\n const created = { id: roomId, name: fallbackName || `Spiral-${roomId.slice(-4)}` };\n spiralState.rooms.push(created);\n return created.id;\n}\n\nfunction syncSpiralCharactersWithWorld(snapshot) {\n const agents = snapshot?.agents || [];\n for (const a of agents) {\n if (!spiralState.characters.has(a.id)) {\n spiralState.characters.set(a.id, {\n id: a.id,\n name: a.id,\n role: \"agent\",\n roomId: spiralState.rooms[0].id,\n source: \"world\"\n });\n }\n }\n}\n\nfunction broadcastExcept(msgObj, exceptSocket) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client !== exceptSocket && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction validateInputFrame(payload, socket) {\n const stats = clientStats.get(socket.clientId) || { lastFrame: 0, windowStart: Date.now(), msgCount: 0 };\n const now = Date.now();\n if (now - stats.windowStart > 5000) {\n stats.windowStart = now;\n stats.msgCount = 0;\n }\n stats.msgCount += 1;\n clientStats.set(socket.clientId, stats);\n if (stats.msgCount > 120) return \"Rate limit exceeded.\";\n\n if (!payload || !Number.isInteger(payload.frame)) return \"frame must be integer.\";\n if (payload.frame <= stats.lastFrame) return \"frame must be monotonic.\";\n const commands = Array.isArray(payload.commands) ? payload.commands : [];\n if (commands.length > 8) return \"Too many commands in one frame.\";\n for (const cmd of commands) {\n if (!cmd || typeof cmd.command !== \"string\") return \"Invalid command format.\";\n if (\n cmd.command !== \"SPAWN_AGENT\" &&\n cmd.command !== \"LIST_CASTLES\" &&\n cmd.command !== \"SPAWN_ENTITY\" &&\n cmd.command !== \"CREATE_CASTLE\"\n ) return \"Unsupported command.\";\n }\n stats.lastFrame = payload.frame;\n clientStats.set(socket.clientId, stats);\n return null;\n}\n\nfunction validateSignalPayload(payload) {\n if (!payload || typeof payload !== \"object\") return \"Invalid signaling payload.\";\n const signalType = payload.signalType;\n if (![\"OFFER\", \"ANSWER\", \"ICE\"].includes(signalType)) return \"Invalid signal type.\";\n if (typeof payload.to !== \"string\" || payload.to.length < 3) return \"Invalid signal destination.\";\n if (signalType !== \"ICE\" && JSON.stringify(payload.sdp || {}).length > 16 * 1024) return \"SDP too large.\";\n if (signalType === \"ICE\" && JSON.stringify(payload.candidate || {}).length > 6 * 1024) return \"ICE too large.\";\n return null;\n}\n\nfunction sanitizeRawMessage(raw) {\n if (!raw) return null;\n const text = raw.toString();\n if (Buffer.byteLength(text, \"utf8\") > MAX_MESSAGE_BYTES) return null;\n return text;\n}\n\nwss.on(\"connection\", async (socket, req) => {\n const origin = req?.headers?.origin || \"\";\n if (!httpCors.isWebSocketOriginAllowed(origin)) {\n socket.close(1008, \"Origin not allowed\");\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const path = req?.url || \"\";\n const query = new URL(`ws://localhost${path}`).searchParams;\n const token = query.get(\"token\");\n if (!token || token !== REQUIRED_GATEWAY_TOKEN) {\n socket.close(1008, \"Invalid gateway token\");\n return;\n }\n }\n if (REQUIRE_AUTH) {\n const result = await verifyClientToken(req);\n if (!result.ok) {\n const isLocal = origin.includes(\"localhost\") || origin.includes(\"127.0.0.1\") || !origin;\n if (!(ALLOW_DEV_ANON && isLocal)) {\n socket.close(1008, result.reason);\n return;\n }\n socket.auth = { ok: true, kind: \"dev-anon\", user: { uid: \"dev-anon\" } };\n } else {\n socket.auth = result;\n }\n }\n\n socket.clientId = `c-${Math.random().toString(36).slice(2, 8)}`;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.HELLO, { message: \"Castle Gateway online.\", clientId: socket.clientId })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.WORLD_SNAPSHOT, orchestrator.worldSnapshot())));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.BROADCAST_STATE, { broadcasterClientId, viewers: [...wss.clients].filter((c) => c.readyState === 1).length })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload())));\n broadcastExcept(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }), socket);\n\n socket.on(\"message\", (raw) => {\n const safeRaw = sanitizeRawMessage(raw);\n if (!safeRaw) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Message too large or invalid.\" })));\n return;\n }\n const parsed = safeJsonParse(safeRaw);\n if (!parsed?.type) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid message.\" })));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND_TEXT) {\n const canonical = parseTextToCommand(parsed.payload?.text);\n if (!canonical) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Unknown command text.\" })));\n return;\n }\n const result = orchestrator.applyCommand(canonical);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND) {\n const result = orchestrator.applyCommand(parsed.payload);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n }\n\n if (parsed.type === WS_MESSAGE.INPUT_FRAME) {\n const err = validateInputFrame(parsed.payload, socket);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const frame = parsed.payload.frame;\n const commands = Array.isArray(parsed.payload?.commands) ? parsed.payload.commands : [];\n orchestrator.queueInputFrame(socket.clientId, frame, commands);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, queued: true, frame })));\n }\n\n if (parsed.type === WS_MESSAGE.SIGNAL) {\n const payload = parsed.payload || {};\n const err = validateSignalPayload(payload);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const signalType = payload.signalType;\n const to = payload.to;\n for (const client of wss.clients) {\n if (client.clientId === to && client.readyState === 1) {\n client.send(JSON.stringify(createEnvelope(WS_MESSAGE.SIGNAL, { ...payload, from: socket.clientId })));\n break;\n }\n }\n }\n\n if (parsed.type === WS_MESSAGE.DRONE_TELEMETRY) {\n const payload = parsed.payload || {};\n const json = JSON.stringify(payload);\n if (Buffer.byteLength(json, \"utf8\") > 48 * 1024) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"DRONE_TELEMETRY payload too large.\" })));\n return;\n }\n broadcastExcept(createEnvelope(WS_MESSAGE.DRONE_TELEMETRY, { ...payload, relayFrom: socket.clientId }), socket);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_START) {\n handleRhizohVoiceLiveStartV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_CHUNK) {\n handleRhizohVoiceLiveChunkV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_STOP) {\n handleRhizohVoiceLiveStopV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_SOCIAL_PULSE) {\n handleCastleSocialPulse(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_WAL_PEER_FEED) {\n handleCastleWalPeerFeed(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.OPEN_DATA_QUERY) {\n queryOpenData(parsed.payload)\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: true, result })));\n })\n .catch((e) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: false, error: e.message })));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_QUERY) {\n runRhizohBrainV2({\n ...(parsed.payload || {}),\n uid: socket?.auth?.user?.uid || \"unknown\"\n })\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, result)));\n })\n .catch(async () => {\n const fallback = await runRhizohBrain(parsed.payload || {}).catch(() => ({\n ok: false,\n reply: \"Rhizoh temporary disturbance. Habitat remains stable.\"\n }));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, fallback)));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.BROADCAST_REGISTER) {\n const role = String(parsed.payload?.role || \"\");\n if (role === \"GENESIS_BROADCAST_AGENT\") {\n broadcasterClientId = socket.clientId;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, registered: role })));\n broadcastState();\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.STUDIO_CUE) {\n // Only registered broadcaster can push global cues.\n if (!broadcasterClientId || socket.clientId !== broadcasterClientId) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Only broadcaster agent can emit STUDIO_CUE.\" })));\n return;\n }\n const cue = parsed.payload || {};\n if (typeof cue !== \"object\") {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid STUDIO_CUE payload.\" })));\n return;\n }\n broadcast(createEnvelope(WS_MESSAGE.STUDIO_CUE, cue));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_JOIN_ROOM) {\n const requested = String(parsed.payload?.roomId || \"\").trim();\n const requestedName = String(parsed.payload?.roomName || \"\").trim();\n const roomId = ensureRoom(requested || `room-${Math.random().toString(36).slice(2, 7)}`, requestedName || null);\n spiralState.activeByClient.set(socket.clientId, roomId);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, roomId })));\n broadcastSpiralState();\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_CREATE_CHARACTER) {\n const name = String(parsed.payload?.name || \"\").trim().slice(0, 32) || `AI-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;\n const role = String(parsed.payload?.role || \"agent\").trim().slice(0, 24) || \"agent\";\n const roomId = ensureRoom(\n String(parsed.payload?.roomId || spiralState.activeByClient.get(socket.clientId) || spiralState.rooms[0].id),\n null\n );\n\n // Character creation always maps to an authoritative world agent.\n const result = orchestrator.applyCommand({\n command: COMMAND.SPAWN_AGENT,\n params: { targetCastleId: \"castle-01\" }\n });\n if (!result?.ok || !result?.data?.id) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Character creation failed.\" })));\n return;\n }\n const id = result.data.id;\n spiralState.characters.set(id, { id, name, role, roomId, source: \"spawned\" });\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, characterId: id, roomId })));\n broadcastSpiralState();\n return;\n }\n });\n\n socket.on(\"close\", () => {\n if (socket.clientId === broadcasterClientId) broadcasterClientId = null;\n clientStats.delete(socket.clientId);\n rhizohVoiceLiveSessions.delete(socket);\n spiralState.activeByClient.delete(socket.clientId);\n removeCastleSocialClientFromAllRooms(socket.clientId);\n broadcast(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }));\n broadcastState();\n });\n});\n\nsetInterval(() => {\n void (async () => {\n try {\n const { frame, snapshot, delta, packedDelta } = orchestrator.tick(1 / 20);\n syncSpiralCharactersWithWorld(snapshot);\n broadcast(createEnvelope(WS_MESSAGE.NETWORK_TICK, { frame }));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_TICK, snapshot));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA, delta));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA_PACKED, packedDelta));\n broadcastSpiralState();\n await runAcademyEventTick();\n const executed = await tickAutomations(Date.now());\n for (const a of executed) {\n await addTranscript(a.uid, {\n source: \"planner\",\n eventType: \"automation-executed\",\n text: `Automation executed: ${a.title} (${a.action})`,\n roomId: \"studio-main\",\n meta: { automationId: a.id, action: a.action, deviceId: a.deviceId || \"\" }\n });\n }\n } catch (e) {\n console.error(\"[GATEWAY] world tick / automations loop error:\", String(e?.message || e));\n }\n })();\n}, 50);\n\n(async () => {\n await rhizohRuntime.telemetry.initOpenTelemetry();\n if (String(process.env.CASTLE_GENESIS_DISK_PERSIST ?? \"\").trim() === \"\") {\n process.env.CASTLE_GENESIS_DISK_PERSIST = \"1\";\n }\n const genesisDiskBoot = await hydrateGenesisContinuityPersistenceBootV0();\n if (!genesisDiskBoot.ok) {\n console.warn(\"[GATEWAY] genesis disk hydrate failed; continuing with in-memory genesis continuity\");\n }\n httpServer.listen(PORT, () => {\n startGenesisCanonicalClock();\n installGenesisCheckpointSurfaceGetter(buildGenesisRuntimeSurfacePayloadLive);\n setGenesisContinuityAfterPublishHook((seq) => {\n noteGenesisCheckpointSeqCommitted(seq);\n noteGenesisContinuitySeqAcceptedV0(seq);\n });\n startGenesisContinuityInfraSampler(buildGenesisRuntimeSurfacePayloadLive, 2000);\n logProductionObservatorySurfaceGuardsV0();\n logGatewaySubstrateAuthorityGuardsV0();\n if (isLlmWorkerEnabledV0()) {\n try {\n const workerBoot = initLlmWorkerAtBootV0();\n console.log(\"[GATEWAY] llm worker boot:\", workerBoot);\n } catch (workerErr) {\n console.error(\"[GATEWAY] llm worker boot failed:\", workerErr);\n }\n }\n console.log(`[GATEWAY] ws/http://localhost:${PORT}`);\n console.log(\n `[GATEWAY] genesis continuity: GET ${rhizohRuntime.routes.genesisRuntime} | POST ${rhizohRuntime.routes.genesisIngress} | SSE ${rhizohRuntime.routes.genesisStream} | replay ${rhizohRuntime.routes.genesisReplay} | diff ${rhizohRuntime.routes.genesisReplayDiff} | equiv ${rhizohRuntime.routes.genesisReplayEquivalence} | analytics ${rhizohRuntime.routes.genesisReplayAnalytics} | evolution ${rhizohRuntime.routes.genesisReplayEvolution}`\n );\n if (String(process.env.CASTLE_GENESIS_OBSERVATORY_LIVE_V01 ?? \"\").trim() === \"1\") {\n console.log(\"GENESIS OBSERVATORY LIVE v0.1 — READ ONLY PHASE STARTED\");\n }\n });\n})().catch((e) => {\n console.error(\"[GATEWAY] fatal boot (before or during listen):\", e);\n process.exit(1);\n});\n"}, "b0eb48b76efd9c9cdfac44f62ff8d47d258d934f": {"t": 1785754151.0197842, "n": 166065, "h": "e45c3e83cf3f9d87", "c": "import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport dotenv from \"dotenv\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconsole.log(\"🔥 [GENESIS_BOOT] entry file:\", __filename);\nconsole.log(\"🔥 [GENESIS_BOOT] node env:\", process.env.NODE_ENV);\nconsole.log(\"🔥 [GENESIS_BOOT] pid:\", process.pid);\nconsole.log(\"🧬 [GENESIS_BOOT] cwd:\", process.cwd());\nconsole.log(\"🧬 [GENESIS_BOOT] import.meta.url:\", import.meta.url);\ndotenv.config({ path: path.join(__dirname, \"..\", \".env\") });\ndotenv.config({ path: path.join(__dirname, \"..\", \".env.local\"), override: true });\n\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport { createServer } from \"node:http\";\nimport { createOrchestrator } from \"@castle/orchestrator\";\nimport { parseTextToCommand } from \"@castle/command-dsl\";\nimport {\n WS_MESSAGE,\n COMMAND,\n createEnvelope,\n createRhizohImmutableEventV0,\n createRhizohPayloadRefV0,\n safeJsonParse,\n validateRhizohVoiceLiveChunkPayloadV0\n} from \"@castle/protocol\";\nimport { queryOpenData } from \"./openData.js\";\nimport { verifyClientToken } from \"./auth.js\";\nimport { runRhizohBrain } from \"./rhizohBrain.js\";\nimport { queryRhizohLlm } from \"./rhizohLlmGateway.js\";\nimport { rhizohGatewayTurn } from \"./rhizohGatewayTurn.js\";\nimport {\n enqueueLlmWorkerTaskAsyncV0,\n getLlmWorkerTaskSnapshotV0,\n isLlmWorkerAsyncEnabledV0,\n isLlmWorkerEnabledV0,\n runLlmWorkerTaskV0\n} from \"./llmWorkerRuntime.js\";\nimport {\n RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n runRhizohVoiceTranscribeV3,\n rhizohVoiceTranscribeEnvV3\n} from \"./rhizohVoiceTranscribeV3.js\";\nimport { createHttpCorsPolicy, normalizeHttpOrigin, parseAllowedOriginsFromEnv } from \"./httpCorsPolicyV1.js\";\nimport {\n applyLanguagePropagationToLlmPayloadV1,\n applyLanguagePropagationToVoiceBodyV1,\n buildRhizohLanguagePropagationEchoV1,\n parseRhizohLanguagePropagationV1\n} from \"./rhizohLanguagePropagationV1.js\";\nimport { trimRhizohLlmGatewayPayloadV1 } from \"./rhizohLlmPayloadTrimV1.js\";\nimport {\n meshAppendDelta,\n meshContinuityAggregate,\n meshJoin,\n meshLeave,\n meshReplayFromNodeId,\n meshReplayFromSeq,\n meshSnapshot,\n meshSubscribeSse\n} from \"./presenceRoomMesh.js\";\nimport { runRhizohBrainV2 } from \"./rhizohBrainV2.js\";\nimport {\n listConnections,\n createConnection,\n updateConnection,\n deleteConnection,\n setDefaultConnection,\n resolveConnection\n} from \"./llmConnectionsStore.js\";\nimport { checkHttpRateLimit, getHttpClientIp } from \"./castleHttpRateLimit.js\";\nimport {\n submitAbuseReportV0,\n listModerationQueueV0,\n softBlockUserV0,\n removeSoftBlockV0,\n listSoftBlockedUsersV0\n} from \"./ops/moderationMvpV0.js\";\nimport { listRecentAgentSnapshotsV0 } from \"./ops/agentObservabilityV0.js\";\nimport { readAgentContainmentConfigV0 } from \"./ops/agentContainmentV0.js\";\nimport { readCostContainmentConfigV0, getCostContainmentStatsV0 } from \"./ops/costContainmentV0.js\";\nimport { getGlobalCostLedgerSnapshotV0, getGclBackendStatusV0 } from \"./ops/globalCostLedgerV0.js\";\nimport { evaluateEconomicStrategyL3V0 } from \"./ops/economicStrategyEngineL3V0.js\";\nimport { getPhasedRolloutStatsV0, readPhasedRolloutTierV0 } from \"./ops/phasedRolloutV0.js\";\nimport { buildUnifiedStateNarrativeV0 } from \"./ops/unifiedStateNarrativeV0.js\";\nimport { resolveHardeningTenantIdV0 } from \"./ops/narrativeTenantIsolationV0.js\";\nimport {\n classifyStressResponseV0,\n verifyStressTaxonomyCoverageV0,\n verifyStressConflictResolutionV0\n} from \"./ops/stressResponseTaxonomyV0.js\";\nimport { verifyResolutionStabilityEnvelopeV0 } from \"./ops/resolutionStabilityEnvelopeV0.js\";\nimport { logLlmAccess } from \"./castleLlmAudit.js\";\nimport { appendMemory, listMemories, getMemoryContext, getPersonaGoalMemory, setPersonaGoalMemory, autoCompactMemories } from \"./memoryStore.js\";\nimport { getFirebasePersistence } from \"./firebasePersistence.js\";\nimport { registerAgentIdentity, listAgentIdentities, getAgentIdentity, updateAgentIdentity } from \"./agentIdentityStore.js\";\nimport { queueAcademyEvent, listAcademyEvents, resolveAcademyEvent, runAcademyEventTick } from \"./academyEventEngine.js\";\nimport { enqueuePdfJob, getPdfJob } from \"./pdfPipeline.js\";\nimport {\n createPublishSession,\n listPublishSessions,\n updatePublishSession,\n addTranscript,\n listTranscripts,\n searchMetadata,\n registerDevice,\n listDevices,\n updateDevice,\n appendTelemetry,\n listTelemetry,\n createAutomation,\n listAutomations,\n tickAutomations,\n enqueueCommand,\n listCommands,\n updateCommandStatus,\n registerSocialChannel,\n listSocialChannels\n} from \"./studioOpsStore.js\";\nimport { metrics as infraMetrics } from \"./infra/metrics.js\";\nimport { renderRhizohPrometheusMetrics, rhizohEnterpriseMetrics } from \"./infra/rhizohEnterpriseMetrics.js\";\nimport { scoreHealth } from \"./infra/healthScore.js\";\nimport { getTraceById, listTracesBySession } from \"./infra/traceRegistry.js\";\nimport { canonicalEpistemicSealString, hashAndSignEpistemicSeal, EPISTEMIC_SEAL_SCHEMA } from \"./epistemicSeal.js\";\nimport { persistEpistemicLedgerBatch } from \"./epistemicLedgerStore.js\";\nimport { persistEpistemicForecastBatch } from \"./epistemicForecastStore.js\";\nimport { buildRhizohExternalGroundTruthPayload } from \"./rhizohExternalGroundTruthGateway.js\";\nimport { ingestRhizohExternalLossBatchHttp } from \"./rhizohExternalLossIngestGateway.js\";\nimport {\n ingestRhizohProductOutcomeHttp,\n listRhizohProductOutcomeAggregates,\n verifyRhizohOutcomeHmac,\n verifyRhizohOutcomeSourceToken\n} from \"./rhizohProductOutcomeIngestGateway.js\";\nimport { initRhizoh } from \"./rhizohProductionBootstrap.js\";\nimport { handleAcademicObservatoryExportGetV0 } from \"./rhizoh/academicObservatoryHttpV0.js\";\nimport {\n handleLiveSportsBundleGetV0,\n handleLiveNewsHeadlinesGetV0,\n handleLiveWorldFeedGetV0\n} from \"./rhizoh/liveWorldFeedHttpV0.js\";\nimport {\n buildGenesisRuntimeSurfacePayload,\n recordGenesisEpistemicLedgerPersisted,\n recordGenesisEpistemicSealIssued,\n startGenesisCanonicalClock\n} from \"./genesisRuntimeSurfaceV0.js\";\nimport { registerGenesisSseClient, getGenesisContinuitySeq, setGenesisContinuityAfterPublishHook } from \"./genesisContinuityStreamHubV0.js\";\nimport { computeGenesisReplayFingerprintV0 } from \"./genesisReplayFingerprintV0.js\";\nimport { buildGenesisGatewayCapabilitiesV0 } from \"./genesisGatewayCapabilitiesV0.js\";\nimport {\n installGenesisCheckpointSurfaceGetter,\n noteGenesisCheckpointSeqCommitted,\n getLatestGenesisSignedCheckpoint\n} from \"./genesisContinuityCheckpointV0.js\";\nimport { hydrateGenesisContinuityPersistenceBootV0 } from \"./genesisContinuityHydrateBootV0.js\";\nimport { startGenesisContinuityInfraSampler } from \"./genesisContinuityInfraSamplerV0.js\";\nimport {\n genesisCheckpointQueryBySeqV0,\n genesisCheckpointQueryRangeV0,\n genesisCheckpointQueryLineageV0\n} from \"./genesisCheckpointQueryV0.js\";\nimport {\n genesisContinuityEventArchiveEnabled,\n queryGenesisContinuityEventArchiveV0,\n GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION\n} from \"./genesisContinuityEventArchiveV0.js\";\nimport { resolveGenesisReplayRouterV1, GENESIS_REPLAY_ROUTER_SCHEMA } from \"./genesisReplayRouterV1.js\";\nimport {\n computeGenesisReplayTemporalDiffV1,\n GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA\n} from \"./genesisReplayTemporalDiffV1.js\";\nimport { compareGenesisReplayEquivalenceV1 } from \"./genesisReplayEquivalenceV1.js\";\nimport { computeGenesisReplayAnalyticsV1 } from \"./genesisReplayAnalyticsV1.js\";\nimport { computeGenesisReplayEvolutionV1 } from \"./genesisReplayEvolutionV1.js\";\nimport { handleStripeCheckoutCreate, handleStripeMembershipWebhook } from \"./stripeWebhookMembership.js\";\nimport { buildRealityHealthPayload } from \"./edgeRealityConsistencyV1.js\";\nimport {\n assertWalPeerSocketAuthorityV0,\n buildGatewaySubstrateAuthoritySnapshotV0,\n logGatewaySubstrateAuthorityGuardsV0,\n validateWalPeerFeedSignatureV0\n} from \"./gatewaySubstrateAuthorityV0.js\";\nimport {\n buildSubstrateOperationalSnapshotV0,\n recordWalPeerRejectV0,\n renderSubstratePrometheusMetricsV0\n} from \"./infra/substrateOperationalMetrics.js\";\nimport { ingestClientSubstrateHealthV0 } from \"./rhizohSubstrateHealthIngestV0.js\";\nimport { recordSubstrateOtelEventV0 } from \"./infra/opentelemetrySubstrateV0.js\";\nimport {\n GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n getGenesisWorldObservationIngressStatusV0,\n ingestGenesisWorldObservationV0\n} from \"./genesisContinuityClientIngressV0.js\";\nimport {\n getGenesisContinuitySeqAuditV0,\n noteGenesisContinuitySeqAcceptedV0\n} from \"./genesisContinuitySeqGapV0.js\";\n\n/** Render / Fly / Railway: `PORT` — yoksa `CASTLE_GATEWAY_PORT` — yoksa 8090. */\nconst PORT =\n Number(process.env.PORT) ||\n Number(process.env.CASTLE_GATEWAY_PORT) ||\n 8090;\n/** Spiral / world WebSocket server — set after `wss` init; used by genesis runtime surface. */\nlet spiralWssForGenesis = null;\nconst MAX_MESSAGE_BYTES = Number(process.env.CASTLE_MAX_MESSAGE_BYTES || 32 * 1024);\nconst REQUIRED_GATEWAY_TOKEN = process.env.CASTLE_GATEWAY_TOKEN || \"\";\nconst ALLOWED_ORIGINS = parseAllowedOriginsFromEnv().fromList;\nconst httpCors = createHttpCorsPolicy();\n\n/** @returns {string|null} `Access-Control-Allow-Origin`; null = browser CORS fail. */\nfunction accessControlAllowOriginValue(req) {\n return httpCors.accessControlAllowOriginValue(req);\n}\n\nfunction applyHttpCorsHeaders(req, res) {\n return httpCors.applyHttpCorsHeaders(req, res);\n}\nconst REQUIRE_AUTH = process.env.CASTLE_REQUIRE_AUTH === \"true\";\nconst ALLOW_DEV_ANON = process.env.CASTLE_ALLOW_DEV_ANON !== \"false\";\nconst ALLOW_DEV_HTTP_UID = process.env.CASTLE_ALLOW_DEV_HTTP_UID !== \"false\";\n/** Presence mesh: allow `X-Castle-Guest-Id` when Firebase auth absent (default on). Set `false` to require auth only. */\nconst ALLOW_MESH_GUEST = process.env.CASTLE_ALLOW_MESH_GUEST !== \"false\";\nconst SOCIAL_RETRY_MAX = Math.max(0, Math.min(5, Number(process.env.CASTLE_SOCIAL_RETRY_MAX || 2)));\nconst SOCIAL_RETRY_BASE_MS = Math.max(100, Number(process.env.CASTLE_SOCIAL_RETRY_BASE_MS || 700));\nconst TELEGRAM_BOT_TOKEN = process.env.CASTLE_TELEGRAM_BOT_TOKEN || \"\";\nconst WHATSAPP_TOKEN = process.env.CASTLE_WHATSAPP_TOKEN || \"\";\nconst WHATSAPP_PHONE_NUMBER_ID = process.env.CASTLE_WHATSAPP_PHONE_NUMBER_ID || \"\";\nconst RL_RHIZOH_LLM_PER_MIN = Math.max(5, Number(process.env.CASTLE_RL_RHIZOH_LLM_PER_MIN || 40));\nconst RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN || 30)\n);\nconst RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN || 120));\nconst RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN = Math.max(\n 10,\n Number(process.env.CASTLE_RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN || 60)\n);\nconst RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN = Math.max(8, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN || 48));\nconst RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN = Math.max(4, Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN || 24));\nconst RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN || 18)\n);\nconst RL_EPISTEMIC_SEAL_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_EPISTEMIC_SEAL_PER_MIN || 120));\nconst RL_EPISTEMIC_LOG_PER_MIN = Math.max(20, Number(process.env.CASTLE_RL_EPISTEMIC_LOG_PER_MIN || 240));\nconst EPISTEMIC_SEAL_SECRET = String(\n process.env.CASTLE_EPISTEMIC_SEAL_SECRET || process.env.CASTLE_GATEWAY_TOKEN || \"\"\n).trim();\nconst RL_LLM_CONN_TEST_PER_MIN = Math.max(2, Number(process.env.CASTLE_RL_LLM_CONNECTION_TEST_PER_MIN || 8));\n\n/** One-shot production warnings: CORS surface, auth toggle, dev bypass flags (observatory deploy hygiene). */\nfunction logProductionObservatorySurfaceGuardsV0() {\n if (String(process.env.NODE_ENV || \"\").toLowerCase() !== \"production\") return;\n const tag = \"[GATEWAY_PROD_GUARD]\";\n const primaryCors = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n const corsListEmpty = ALLOWED_ORIGINS.length === 0;\n if (corsListEmpty && !primaryCors) {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty and CASTLE_HTTP_CORS_ORIGIN empty — production still merges RHIZOH_BROWSER_ORIGINS_V1 defaults; set env for preview/staging hosts`\n );\n } else if (corsListEmpty && primaryCors && primaryCors !== \"*\") {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty — only CASTLE_HTTP_CORS_ORIGIN set; add web.app + firebaseapp.com (and previews) to CASTLE_ALLOWED_ORIGINS if clients use alternate hostnames`\n );\n }\n if (primaryCors && /localhost|127\\.0\\.0\\.1/i.test(primaryCors)) {\n console.warn(`${tag} CORS: CASTLE_HTTP_CORS_ORIGIN is localhost while NODE_ENV=production`);\n }\n if (REQUIRE_AUTH) {\n console.warn(\n `${tag} AUTH: CASTLE_REQUIRE_AUTH=true — clients must send a valid gateway credential (otherwise silent 401 on replay/SSE); EventSource cannot set custom headers unless proxied`\n );\n if (REQUIRED_GATEWAY_TOKEN.length < 16) {\n console.warn(`${tag} AUTH: CASTLE_GATEWAY_TOKEN missing or short while require-auth enabled`);\n }\n }\n if (ALLOW_DEV_ANON) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_ANON is not \"false\" — dev anonymous path may be live in production (set CASTLE_ALLOW_DEV_ANON=false)`\n );\n }\n if (ALLOW_DEV_HTTP_UID) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_HTTP_UID is not \"false\" — X-Castle-Dev-Uid bypass risk (set CASTLE_ALLOW_DEV_HTTP_UID=false)`\n );\n }\n}\n\nconst rhizohRuntime = initRhizoh();\n\n/** İstemci: llmKeySource veya keySource — env | user_connection | auto */\nfunction normalizeClientLlmKeySource(raw) {\n const s = String(raw ?? \"\").trim().toLowerCase();\n if (s === \"env\" || s === \"server\") return \"env\";\n if (s === \"user_connection\" || s === \"connection\" || s === \"user\") return \"user_connection\";\n if (s === \"auto\" || s === \"\") return \"auto\";\n return null;\n}\n\nfunction sendJson(res, statusCode, payload) {\n res.writeHead(statusCode, { \"Content-Type\": \"application/json; charset=utf-8\" });\n res.end(JSON.stringify(payload));\n}\n\nfunction logRhizohHealth(stage, detail = {}) {\n try {\n console.info(`[CASTLE_${String(stage || \"unknown\")}]`, detail && typeof detail === \"object\" ? detail : {});\n } catch {\n /* noop */\n }\n}\n\nfunction renderInfraMetricsProm() {\n const lines = [\n `castle_gateway_events_processed_total ${infraMetrics.eventsProcessed}`,\n `castle_gateway_duplicate_reject_total ${infraMetrics.duplicateRejects}`,\n `castle_gateway_enqueue_latency_ms ${infraMetrics.enqueueLatencyMs}`,\n `castle_gateway_queue_depth ${infraMetrics.queueDepth}`,\n `castle_gateway_queue_lag_seconds ${(Number(infraMetrics.queueLag || 0) / 1000).toFixed(3)}`,\n `castle_gateway_errors_total ${infraMetrics.errors}`\n ];\n const buckets = infraMetrics.enqueueLatencyBuckets || {};\n for (const k of Object.keys(buckets)) {\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"${k}\"} ${buckets[k]}`);\n }\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"+Inf\"} ${infraMetrics.eventsProcessed}`);\n if (process.env.CASTLE_SUBSTRATE_METRICS !== \"0\") {\n lines.push(renderSubstratePrometheusMetricsV0().trimEnd());\n }\n return lines.join(\"\\n\");\n}\n\nasync function fetchJsonWithTimeout(url, timeoutMs = 1200) {\n const ctl = new AbortController();\n const tid = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const res = await fetch(url, { signal: ctl.signal });\n if (!res.ok) return null;\n return await res.json();\n } catch {\n return null;\n } finally {\n clearTimeout(tid);\n }\n}\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction getHttpPathname(req) {\n let p = String(req.url || \"\").split(\"?\")[0];\n /* Reverse proxies / clients sometimes append `/` — avoid silent 404 on e.g. `/rhizoh/genesis/stream/`. */\n while (p.length > 1 && p.endsWith(\"/\")) {\n p = p.slice(0, -1);\n }\n return p;\n}\n\nfunction rhizohLlmEnvConfigured() {\n const p = String(process.env.CASTLE_LLM_PROVIDER || \"openai\").toLowerCase();\n const key =\n p === \"anthropic\"\n ? process.env.ANTHROPIC_API_KEY\n : p === \"gemini\"\n ? process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY\n : p === \"xai\"\n ? process.env.XAI_API_KEY\n : p === \"deepseek\"\n ? process.env.DEEPSEEK_API_KEY\n : p === \"mistral\"\n ? process.env.MISTRAL_API_KEY\n : p === \"openrouter\"\n ? process.env.OPENROUTER_API_KEY\n : process.env.OPENAI_API_KEY;\n return !!String(key || \"\").trim();\n}\n\n/**\n * @param {import(\"firebase-admin/firestore\").Firestore | null} db\n * @param {number} timeoutMs\n */\nasync function probeFirestoreReachable(db, timeoutMs = 2000) {\n if (!db) return { ok: false, reason: \"no_db\" };\n const timeout = sleep(timeoutMs).then(() => ({ ok: false, reason: \"timeout\" }));\n const work = db\n .collection(\"_castle_gateway_health\")\n .doc(\"ping\")\n .get()\n .then(() => ({ ok: true, reason: \"reachable\" }))\n .catch((e) => ({ ok: false, reason: String(e?.message || e || \"firestore_error\") }));\n return Promise.race([work, timeout]);\n}\n\nfunction stripHtml(text = \"\") {\n return String(text)\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \" \")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \" \")\n .replace(/<[^>]+>/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction haversineMeters(aLat, aLon, bLat, bLon) {\n const R = 6371000;\n const dLat = ((bLat - aLat) * Math.PI) / 180;\n const dLon = ((bLon - aLon) * Math.PI) / 180;\n const sa = Math.sin(dLat / 2);\n const sb = Math.sin(dLon / 2);\n const x = sa * sa + Math.cos((aLat * Math.PI) / 180) * Math.cos((bLat * Math.PI) / 180) * sb * sb;\n return 2 * R * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));\n}\n\nasync function checkPublishTargetHealth(protocol, target) {\n const p = String(protocol || \"\").toUpperCase();\n const t = String(target || \"\").trim();\n if (!t) return { ok: false, reason: \"target_missing\", shouldReconnect: false };\n if (p === \"RTMP\") {\n const ok = t.startsWith(\"rtmp://\") || t.startsWith(\"rtmps://\");\n return { ok, reason: ok ? \"rtmp_target_format_ok\" : \"invalid_rtmp_target\", shouldReconnect: !ok, reconnectInSec: 15 };\n }\n if (p === \"WHIP\" || p === \"SFU\") {\n try {\n const res = await fetch(t, { method: \"OPTIONS\" });\n const ok = res.status < 500;\n return { ok, reason: `http_${res.status}`, shouldReconnect: !ok, reconnectInSec: 8 };\n } catch {\n return { ok: false, reason: \"network_unreachable\", shouldReconnect: true, reconnectInSec: 8 };\n }\n }\n return { ok: true, reason: \"unchecked_protocol\", shouldReconnect: false };\n}\n\nasync function fetchTextFromUrl(url) {\n const res = await fetch(url, { method: \"GET\" });\n if (!res.ok) throw new Error(`fetch_${res.status}`);\n const ct = String(res.headers.get(\"content-type\") || \"\");\n if (ct.includes(\"application/pdf\")) throw new Error(\"pdf_binary_not_supported_directly\");\n const body = await res.text();\n return stripHtml(body).slice(0, 14000);\n}\n\nfunction buildTaskActionGraph(task = \"\", context = {}) {\n const t = String(task || \"\").toLowerCase();\n if (t.includes(\"dinner\") || t.includes(\"akşam yemeği\") || t.includes(\"aksam yemegi\")) {\n return {\n taskTitle: \"Dinner Prep Planner\",\n nodes: [\n { id: \"n1\", title: \"Kitchen preheat\", action: \"smart-oven.preheat\", params: { temperatureC: 190, mode: \"fan\" } },\n { id: \"n2\", title: \"Lighting comfort\", action: \"smart-light.set_scene\", params: { scene: \"warm_dinner\" } },\n { id: \"n3\", title: \"Fridge inventory check\", action: \"smart-fridge.inventory_scan\", params: {} },\n { id: \"n4\", title: \"Reminder notification\", action: \"smart-watch.notify\", params: { text: \"Dinner prep started.\" } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" },\n { from: \"n3\", to: \"n4\" }\n ]\n };\n }\n return {\n taskTitle: task || \"General Planner\",\n nodes: [\n { id: \"n1\", title: \"Safety check\", action: \"device.safety_check\", params: { context } },\n { id: \"n2\", title: \"Primary action\", action: \"device.execute_task\", params: { task } },\n { id: \"n3\", title: \"Report\", action: \"studio.timeline_note\", params: { task } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" }\n ]\n };\n}\n\nfunction buildEthicsPrograms() {\n return {\n policyVersion: \"v1\",\n tracks: [\n {\n id: \"elderly-care-assist\",\n audience: \"elderly\",\n title: \"Elderly Assistance Track\",\n principles: [\"human-in-the-loop\", \"consent-first\", \"low-friction alerts\"],\n modules: [\"medication-reminder\", \"fall-risk-check\", \"family-notify\", \"daily-companion-brief\"]\n },\n {\n id: \"kids-safe-learning\",\n audience: \"children\",\n title: \"Kids Safe Learning Track\",\n principles: [\"guardian-consent\", \"age-appropriate-language\", \"content-filtering\"],\n modules: [\"study-coach\", \"screen-time-balance\", \"safe-social-share\", \"emergency-contact\"]\n }\n ]\n };\n}\n\nasync function deliverViaTelegram(channel, text) {\n const chatId = String(channel.endpoint || \"\").trim();\n if (!TELEGRAM_BOT_TOKEN) throw new Error(\"telegram_token_missing\");\n if (!chatId) throw new Error(\"telegram_chat_id_missing\");\n const url = `https://api.telegram.org/bot${encodeURIComponent(TELEGRAM_BOT_TOKEN)}/sendMessage`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ chat_id: chatId, text })\n });\n if (!res.ok) throw new Error(`telegram_http_${res.status}`);\n return { provider: \"telegram\", status: res.status };\n}\n\nasync function deliverViaWhatsapp(channel, text) {\n const to = String(channel.endpoint || \"\").trim();\n if (!WHATSAPP_TOKEN) throw new Error(\"whatsapp_token_missing\");\n if (!WHATSAPP_PHONE_NUMBER_ID) throw new Error(\"whatsapp_phone_number_id_missing\");\n if (!to) throw new Error(\"whatsapp_to_missing\");\n const url = `https://graph.facebook.com/v22.0/${encodeURIComponent(WHATSAPP_PHONE_NUMBER_ID)}/messages`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${WHATSAPP_TOKEN}` },\n body: JSON.stringify({ messaging_product: \"whatsapp\", to, type: \"text\", text: { body: text } })\n });\n if (!res.ok) throw new Error(`whatsapp_http_${res.status}`);\n return { provider: \"whatsapp\", status: res.status };\n}\n\nasync function deliverViaWebhook(channel, text) {\n const endpoint = String(channel.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"webhook_endpoint_missing\");\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text, platform: channel.platform || \"webhook\", ts: Date.now() })\n });\n if (!res.ok) throw new Error(`webhook_http_${res.status}`);\n return { provider: \"webhook\", status: res.status };\n}\n\nasync function deliverSocialMessage(channel, text) {\n const platform = String(channel.platform || \"webhook\").toLowerCase();\n if (platform === \"telegram\") return deliverViaTelegram(channel, text);\n if (platform === \"whatsapp\") return deliverViaWhatsapp(channel, text);\n return deliverViaWebhook(channel, text);\n}\n\nasync function deliverSocialWithRetry(channel, text) {\n let attempt = 0;\n let lastError = null;\n while (attempt <= SOCIAL_RETRY_MAX) {\n try {\n const result = await deliverSocialMessage(channel, text);\n return { ok: true, attempt: attempt + 1, ...result };\n } catch (error) {\n lastError = error;\n if (attempt >= SOCIAL_RETRY_MAX) break;\n const jitter = Math.floor(Math.random() * 90);\n await sleep(SOCIAL_RETRY_BASE_MS * Math.pow(2, attempt) + jitter);\n }\n attempt += 1;\n }\n return { ok: false, attempt: attempt + 1, error: String(lastError?.message || \"delivery_failed\") };\n}\n\nasync function dispatchCommandToAdapter(device, command) {\n const adapter = String(command?.adapter || device?.adapter || \"websocket\").toLowerCase();\n const endpoint = String(device?.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"device_endpoint_missing\");\n if (adapter === \"mqtt\") {\n // MQTT delivery is proxied via a bridge endpoint (HTTP webhook style).\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n type: \"mqtt-command\",\n topic: `castle/devices/${device.id}/commands`,\n action: command.action,\n params: command.params || {}\n })\n });\n if (!res.ok) throw new Error(`mqtt_bridge_http_${res.status}`);\n return { ok: true, adapter, via: \"http-bridge\", status: res.status };\n }\n if (!endpoint.startsWith(\"ws://\") && !endpoint.startsWith(\"wss://\")) throw new Error(\"invalid_websocket_endpoint\");\n const sendResult = await new Promise((resolve, reject) => {\n const ws = new WebSocket(endpoint);\n const timer = setTimeout(() => {\n try {\n ws.close();\n } catch {\n /* noop */\n }\n reject(new Error(\"websocket_timeout\"));\n }, 3000);\n ws.on(\"open\", () => {\n ws.send(\n JSON.stringify({\n type: \"castle-command\",\n commandId: command.id,\n deviceId: command.deviceId,\n action: command.action,\n params: command.params || {}\n }),\n (err) => {\n clearTimeout(timer);\n ws.close();\n if (err) reject(new Error(\"websocket_send_failed\"));\n else resolve({ ok: true, adapter: \"websocket\", via: \"ws\" });\n }\n );\n });\n ws.on(\"error\", () => {\n clearTimeout(timer);\n reject(new Error(\"websocket_connect_failed\"));\n });\n });\n return sendResult;\n}\n\nfunction readHttpJson(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => {\n if (!buf) return resolve({});\n const payload = safeJsonParse(buf);\n if (!payload || typeof payload !== \"object\") return reject(new Error(\"invalid_json_payload\"));\n resolve(payload);\n });\n req.on(\"error\", reject);\n });\n}\n\n/** Ham gövde (HMAC doğrulaması için). */\nfunction readHttpBodyText(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => resolve(buf));\n req.on(\"error\", reject);\n });\n}\n\nfunction maskOpaqueId(value) {\n const t = String(value || \"\");\n if (!t) return \"(empty)\";\n if (t.length <= 8) return \"****\";\n return `${t.slice(0, 4)}…${t.slice(-2)}`;\n}\n\nfunction readGatewayHttpToken(req) {\n const x = String(req.headers?.[\"x-castle-gateway-token\"] || \"\").trim();\n if (x) return x;\n const auth = String(req.headers?.authorization || \"\");\n const m = /^Bearer\\s+(.+)$/i.exec(auth);\n return m ? String(m[1] || \"\").trim() : \"\";\n}\n\nasync function resolveHttpUser(req) {\n const authResult = await verifyClientToken(req);\n if (authResult?.ok && authResult.user?.uid) {\n return { ok: true, uid: String(authResult.user.uid), mode: authResult.kind || \"auth\" };\n }\n if (ALLOW_DEV_ANON && ALLOW_DEV_HTTP_UID) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) return { ok: true, uid: `dev-${devUid.slice(0, 64)}`, mode: \"dev-header\" };\n }\n return { ok: false, reason: authResult?.reason || \"auth_required\" };\n}\n\n/**\n * Epistemic ledger ingest — Firebase uid OR valid transport token + X-Castle-Dev-Uid.\n * Prod disables CASTLE_ALLOW_DEV_HTTP_UID; LLM may still work while telemetry 401'd without this.\n */\nasync function resolveEpistemicIngestActor(req) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok && tok === REQUIRED_GATEWAY_TOKEN) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) {\n return { ok: true, uid: `gw-telemetry-${devUid.slice(0, 64)}`, mode: \"gateway-token-dev-uid\" };\n }\n return { ok: false, reason: \"dev_uid_required\" };\n }\n if (tok) {\n return { ok: false, reason: \"gateway_token_mismatch\" };\n }\n }\n const auth = await resolveHttpUser(req);\n if (auth.ok) return auth;\n return { ok: false, reason: auth.reason || \"auth_required\" };\n}\n\n/** Mesh actor: Firebase uid, dev header, or `guest:<uuid>` via header / `guestId` query (SSE cannot set headers). */\nasync function resolveMeshActor(req) {\n const auth = await resolveHttpUser(req);\n if (auth.ok) return { ok: true, clientUid: auth.uid, mode: auth.mode };\n if (ALLOW_MESH_GUEST) {\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const gid = String(req.headers?.[\"x-castle-guest-id\"] || q.searchParams.get(\"guestId\") || \"\").trim();\n if (gid) {\n const normalized = gid.startsWith(\"guest:\") ? gid.slice(0, 180) : `guest:${gid.slice(0, 160)}`;\n return { ok: true, clientUid: normalized, mode: \"guest-header\" };\n }\n }\n return { ok: false, reason: \"mesh_auth_required\" };\n}\n\n/** Live genesis runtime JSON (GET /rhizoh/genesis/runtime + SSE infra sampler). */\nasync function buildGenesisRuntimeSurfacePayloadLive() {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n let spiralWsActive = 0;\n if (spiralWssForGenesis) {\n for (const c of spiralWssForGenesis.clients) {\n if (c.readyState === 1) spiralWsActive += 1;\n }\n }\n const base = buildGenesisRuntimeSurfacePayload({\n infraMetrics,\n scoredHealth: scored,\n rhizohEnterpriseMetrics,\n mesh: meshContinuityAggregate(),\n workerHealth: worker,\n port: PORT,\n spiralWebSocketClientsActive: spiralWsActive\n });\n return {\n ...base,\n replayFingerprint: computeGenesisReplayFingerprintV0(base),\n gatewayCapabilities: buildGenesisGatewayCapabilitiesV0(),\n genesisStream: {\n schema: \"castle.genesis.stream_cursor.v0\",\n lastAcceptedSeq: getGenesisContinuitySeq(),\n clientIngress: {\n schema: GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n contract: \"castle.world_observation.ingress_envelope.v1\",\n postPath: rhizohRuntime.routes.genesisIngress,\n closure: getGenesisWorldObservationIngressStatusV0()\n },\n seqContinuity: getGenesisContinuitySeqAuditV0(),\n eventArchive: genesisContinuityEventArchiveEnabled()\n ? {\n enabled: true,\n projection: GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION,\n queryPath: rhizohRuntime.routes.genesisContinuityEvents\n }\n : { enabled: false },\n replayRouter: {\n schema: GENESIS_REPLAY_ROUTER_SCHEMA,\n queryPath: rhizohRuntime.routes.genesisReplay,\n queryHint: \"from=&to= or range=from-to; optional type=&checkpoints=0\",\n temporalDiffPath: rhizohRuntime.routes.genesisReplayDiff,\n temporalDiffSchema: GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA,\n equivalencePath: rhizohRuntime.routes.genesisReplayEquivalence,\n analyticsPath: rhizohRuntime.routes.genesisReplayAnalytics,\n evolutionPath: rhizohRuntime.routes.genesisReplayEvolution\n }\n }\n };\n}\n\nconst httpServer = createServer(async (req, res) => {\n const corsAllow = applyHttpCorsHeaders(req, res);\n if (req.method === \"OPTIONS\") {\n if (!corsAllow && req.headers?.origin) {\n console.warn(\"[GATEWAY_CORS] preflight rejected for origin:\", req.headers.origin);\n }\n res.writeHead(corsAllow ? 204 : 403);\n res.end();\n return;\n }\n\n const pathname = getHttpPathname(req);\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/genesis/__ping\") {\n sendJson(res, 200, {\n ok: true,\n pid: process.pid,\n entry: __filename,\n time: Date.now(),\n genesis: true\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/epistemic/__probe\") {\n sendJson(res, 200, {\n ok: true,\n epistemicIngestActorV2: true,\n routes: {\n seal: rhizohRuntime.routes.epistemicSeal,\n logsBatch: rhizohRuntime.routes.epistemicLogsBatch\n },\n hasGatewayToken: Boolean(REQUIRED_GATEWAY_TOKEN),\n gatewayTokenLen: REQUIRED_GATEWAY_TOKEN ? REQUIRED_GATEWAY_TOKEN.length : 0\n });\n return;\n }\n\n if (\n req.method === \"GET\" &&\n (pathname === \"/rhizoh/live/world-feed\" ||\n pathname === \"/rhizoh/live/sports-bundle\" ||\n pathname === \"/rhizoh/live/news-headlines\")\n ) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_live_world_feed:${ip}`, RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const out =\n pathname === \"/rhizoh/live/sports-bundle\"\n ? await handleLiveSportsBundleGetV0()\n : pathname === \"/rhizoh/live/news-headlines\"\n ? await handleLiveNewsHeadlinesGetV0(req)\n : await handleLiveWorldFeedGetV0(req);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/webhooks/stripe\") {\n await handleStripeMembershipWebhook(req, res);\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/billing/stripe/checkout\") {\n await handleStripeCheckoutCreate(req, res, { readHttpJson, resolveHttpUser, sendJson });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/live\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, live: false, dns: true, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n sendJson(res, 200, { ok: true, live: true, dns: true, service: \"castle-gateway\", ts: Date.now() });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/cors\") {\n const reqOrigin = normalizeHttpOrigin(req.headers?.origin);\n const allowOrigin = accessControlAllowOriginValue(req);\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n requestOrigin: reqOrigin || null,\n allowed: Boolean(allowOrigin),\n allowOrigin: allowOrigin || null,\n allowSetSize: httpCors.allowSet.size\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/ready\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, ready: false, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n try {\n const persistence = getFirebasePersistence();\n const llmProbe = rhizohLlmEnvConfigured();\n sendJson(res, 200, {\n ok: true,\n ready: true,\n dns: true,\n persistence: persistence.mode,\n service: \"castle-gateway\",\n gateway: \"ok\",\n llm: llmProbe ? \"ok\" : \"degraded\",\n spine: llmProbe ? \"ok\" : \"partial\"\n });\n } catch (e) {\n sendJson(res, 503, { ok: false, ready: false, reason: String(e?.message || e), service: \"castle-gateway\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/reality\") {\n const payload = buildRealityHealthPayload();\n sendJson(res, payload.ok ? 200 : 503, payload);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/substrate\") {\n const authority = buildGatewaySubstrateAuthoritySnapshotV0();\n const operational = buildSubstrateOperationalSnapshotV0();\n const drift = buildRealityHealthPayload();\n const ok = Boolean(authority.productionAuthoritySatisfied) && drift.ok;\n sendJson(res, ok ? 200 : 503, {\n ok,\n service: \"castle-gateway\",\n ts: Date.now(),\n authority,\n operational,\n drift: { ok: drift.ok, errors: drift.drift?.errors, warnings: drift.drift?.warnings }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/substrate/health\") {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const body = await readHttpJson(req);\n const uid = (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestClientSubstrateHealthV0(body, { uid, clientId: uid });\n if (!result.ok) {\n sendJson(res, 400, result);\n return;\n }\n recordSubstrateOtelEventV0(\"client_health_ingest\", { uid: String(uid).slice(0, 32) });\n sendJson(res, 200, result);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/deps\") {\n const t0 = Date.now();\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, {\n ok: false,\n dns: true,\n llm: false,\n firestore: false,\n phase: \"maintenance\",\n latencyMs: Date.now() - t0,\n service: \"castle-gateway\"\n });\n return;\n }\n const persistence = getFirebasePersistence();\n let firestoreOk = persistence.mode !== \"firebase\";\n let firestoreDetail = persistence.mode === \"file\" ? \"file_mode\" : \"not_applicable\";\n if (persistence.mode === \"firebase\" && persistence.db) {\n const pr = await probeFirestoreReachable(persistence.db, 2000);\n firestoreOk = pr.ok;\n firestoreDetail = pr.reason || (pr.ok ? \"reachable\" : \"fail\");\n } else if (persistence.mode === \"firebase\" && !persistence.db) {\n firestoreOk = false;\n firestoreDetail = \"firebase_not_initialized\";\n }\n const llmOk = rhizohLlmEnvConfigured();\n const overallOk = llmOk && firestoreOk;\n const latencyMs = Date.now() - t0;\n sendJson(res, 200, {\n ok: overallOk,\n dns: true,\n llm: llmOk,\n firestore: persistence.mode === \"firebase\" ? firestoreOk : false,\n persistence: persistence.mode,\n firestoreDetail,\n latencyMs,\n service: \"castle-gateway\",\n wsPort: PORT\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health\") {\n const persistence = getFirebasePersistence();\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n wsPort: PORT,\n persistence: persistence.mode,\n presenceMesh: \"sse\"\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/health\") {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n sendJson(res, 200, {\n ok: true,\n role: \"gateway\",\n queueLagMs: infraMetrics.queueLag,\n queueDepth: infraMetrics.queueDepth,\n errors: infraMetrics.errors,\n status: scored.status,\n score: scored.score,\n reasons: scored.reasons,\n worker: worker\n ? {\n status: worker.status,\n score: worker.score,\n reasons: worker.reasons\n }\n : null,\n timestamp: Date.now()\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisRuntime) {\n const payload = await buildGenesisRuntimeSurfacePayloadLive();\n sendJson(res, 200, payload);\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.genesisIngress) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const actor = await resolveMeshActor(req);\n const clientId = actor.ok ? actor.clientUid : (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestGenesisWorldObservationV0(body, { clientId });\n if (!result.ok) {\n const status =\n result.error === \"rate_limited\" || result.error === \"agent_spoke_throttled\"\n ? 429\n : result.error === \"payload_too_large\"\n ? 413\n : 400;\n sendJson(res, status, result);\n return;\n }\n sendJson(res, result.idempotent ? 200 : 202, result);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisStream) {\n const corsOrigin = accessControlAllowOriginValue(req);\n if (!corsOrigin && String(process.env.NODE_ENV || \"\").toLowerCase() === \"production\") {\n const og = String(req.headers?.origin || \"\").trim();\n if (og) {\n console.warn(\"[GATEWAY] genesis SSE: missing Access-Control-Allow-Origin for browser Origin (CORS whitelist mismatch):\", og);\n }\n }\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n res.write(\"retry: 3000\\n\\n\");\n res.write(\": genesis continuity stream\\n\\n\");\n res.write(\"event: genesis\\n\");\n res.write(`data: {\"booted\":true,\"pid\":${process.pid}}\\n\\n`);\n registerGenesisSseClient(res, req);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLatest) {\n const cp = getLatestGenesisSignedCheckpoint();\n if (!cp) {\n sendJson(res, 404, { ok: false, error: \"no_checkpoint_yet\", hint: \"wait_for_seq_interval_or_configure_signing_secret\" });\n return;\n }\n sendJson(res, 200, { ok: true, checkpoint: cp });\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(rhizohRuntime.routes.genesisCheckpointBySeqPrefix)) {\n const tail = pathname.slice(rhizohRuntime.routes.genesisCheckpointBySeqPrefix.length).split(\"/\")[0];\n const seq = parseInt(String(tail || \"\").trim(), 10);\n const out = await genesisCheckpointQueryBySeqV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"checkpoint_not_found\") {\n sendJson(res, 404, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointRange) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryRangeV0(from, to);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"range_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLineage) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const seq = parseInt(String(u.searchParams.get(\"seq\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryLineageV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"lineage_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisContinuityEvents) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const limit = parseInt(String(u.searchParams.get(\"limit\") || \"\").trim(), 10);\n const out = await queryGenesisContinuityEventArchiveV0(from, to, type, limit);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\" || out.error === \"event_archive_disabled\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"archive_file_too_large\") {\n sendJson(res, 413, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplay) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await resolveGenesisReplayRouterV1({\n from: fromSeq,\n to: toSeq,\n type,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayDiff) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const out = await computeGenesisReplayTemporalDiffV1({ from: fromSeq, to: toSeq, type });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEquivalence) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from1 = parseInt(String(u.searchParams.get(\"from1\") || \"\").trim(), 10);\n const to1 = parseInt(String(u.searchParams.get(\"to1\") || \"\").trim(), 10);\n const from2 = parseInt(String(u.searchParams.get(\"from2\") || \"\").trim(), 10);\n const to2 = parseInt(String(u.searchParams.get(\"to2\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await compareGenesisReplayEquivalenceV1({\n from1,\n to1,\n from2,\n to2,\n type,\n includeCheckpoints\n });\n if (\n !out.ok &&\n (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")\n ) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayAnalytics) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayAnalyticsV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEvolution) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const collapseWindows = parseInt(String(u.searchParams.get(\"collapseWindows\") || \"0\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayEvolutionV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n collapseWindows,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/metrics\") {\n res.writeHead(200, { \"Content-Type\": \"text/plain; version=0.0.4; charset=utf-8\" });\n res.end(renderInfraMetricsProm());\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.infraPrometheusMetrics) {\n if (process.env.CASTLE_PROMETHEUS_METRICS !== \"1\") {\n sendJson(res, 404, { ok: false, error: \"prometheus_metrics_disabled\" });\n return;\n }\n res.writeHead(200, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(renderRhizohPrometheusMetrics());\n return;\n }\n if (req.method === \"GET\" && pathname.startsWith(\"/infra/traces/\")) {\n const traceId = decodeURIComponent(pathname.slice(\"/infra/traces/\".length));\n const item = await getTraceById(traceId);\n if (!item) return sendJson(res, 404, { ok: false, error: \"trace_not_found\" });\n return sendJson(res, 200, { ok: true, trace: item });\n }\n if (req.method === \"GET\" && pathname === \"/infra/traces\") {\n const u = new URL(String(req.url || \"/infra/traces\"), `http://${req.headers.host || \"localhost\"}`);\n const sessionId = String(u.searchParams.get(\"sessionId\") || \"\").trim();\n const items = sessionId ? await listTracesBySession(sessionId) : [];\n return sendJson(res, 200, { ok: true, sessionId, traces: items });\n }\n\n /** Presence room mesh (C): JOIN / LEAVE / SNAPSHOT / DELTA / REPLAY (HTTP); SUBSCRIBE = SSE stream. */\n if (req.method === \"POST\" && pathname === \"/presence/mesh/join\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid, clientUid: actor.clientUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/leave\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshLeave(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/snapshot\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n sendJson(res, 200, meshSnapshot(roomUid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/delta\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, MAX_MESSAGE_BYTES);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const seq = meshAppendDelta(roomUid, actor.clientUid, {\n node: body?.node,\n projectionPatch: body?.projectionPatch,\n writerSubject: body?.writerSubject\n });\n sendJson(res, 200, { ok: true, roomUid, seq });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/replay\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const fromNodeId = body?.fromNodeId != null ? String(body.fromNodeId) : \"\";\n const fromSeq = body?.fromSeq;\n if (fromNodeId) {\n sendJson(res, 200, meshReplayFromNodeId(roomUid, fromNodeId));\n return;\n }\n sendJson(res, 200, meshReplayFromSeq(roomUid, fromSeq));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/presence/mesh/subscribe\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const roomUid = String(q.searchParams.get(\"roomUid\") || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n const write = (chunk) => {\n try {\n res.write(chunk);\n } catch {\n /* closed */\n }\n };\n const unsub = meshSubscribeSse(roomUid, write);\n const onClose = () => {\n unsub();\n meshLeave(roomUid, actor.clientUid);\n try {\n res.end();\n } catch {\n /* ignore */\n }\n };\n req.on(\"close\", onClose);\n req.on(\"aborted\", onClose);\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, uid: auth.uid, mode: auth.mode, items: await listConnections(auth.uid) });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const id = await createConnection(auth.uid, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"create_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n await updateConnection(auth.uid, id, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"update_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"DELETE\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n await deleteConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"delete_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections/test\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n if (!checkHttpRateLimit(`llm_conn_test:${auth.uid}`, RL_LLM_CONN_TEST_PER_MIN, 60_000)) {\n return sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n }\n const hasBodyKey = !!String(payload?.apiKey || \"\").trim();\n const result = await queryRhizohLlm(\n {\n message: String(payload?.message || \"Ping from Castle Gateway\"),\n context: payload?.context || { source: \"connection_test\" },\n provider: payload?.provider,\n model: payload?.model,\n apiKey: payload?.apiKey || \"\"\n },\n { allowExternalApiKey: true, llmKeySource: hasBodyKey ? \"auto\" : \"env\" }\n );\n logLlmAccess({\n route: \"/llm/connections/test\",\n uid: auth.uid,\n llmKeyBillingOwner: result.llmKeyBillingOwner,\n llmKeyOrigin: result.llmKeyOrigin,\n provider: result.provider,\n model: result.model,\n connectionId: null\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"connection_test_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAgentIdentities(auth.uid);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerAgentIdentity({\n ownerUid: auth.uid,\n agentId: payload?.agentId,\n role: payload?.role,\n personaSeed: payload?.personaSeed || {},\n capabilityLevel: payload?.capabilityLevel\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/agents/identities/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const owns = await getAgentIdentity(id);\n if (!owns || owns.ownerUid !== auth.uid) return sendJson(res, 403, { ok: false, error: \"forbidden\" });\n const payload = await readHttpJson(req);\n const row = await updateAgentIdentity(id, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/academy/events\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/academy/events\"}`);\n const status = String(url.searchParams.get(\"status\") || \"\");\n const items = await listAcademyEvents({ ownerUid: auth.uid, status, limit: 120 });\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/academy/events\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const ev = await queueAcademyEvent({\n type: payload?.type,\n roomId: payload?.roomId,\n ownerUid: auth.uid,\n participants: payload?.participants || [],\n topic: payload?.topic || \"academy\",\n payload: payload?.payload || {}\n });\n sendJson(res, 200, { ok: true, event: ev });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && String(req.url || \"\").startsWith(\"/academy/events/\") && String(req.url || \"\").endsWith(\"/resolve\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const parts = String(req.url || \"\").split(\"/\").filter(Boolean);\n const eventId = parts[2] || \"\";\n try {\n const payload = await readHttpJson(req);\n const out = await resolveAcademyEvent(eventId, payload || {});\n sendJson(res, 200, { ok: true, event: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_resolve_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/memory\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/memory\"}`);\n const scope = url.searchParams.get(\"scope\") === \"agents\" ? \"agents\" : \"users\";\n const id = String(url.searchParams.get(\"id\") || (scope === \"agents\" ? \"\" : auth.uid));\n const kind = String(url.searchParams.get(\"kind\") || \"\");\n const limit = Number(url.searchParams.get(\"limit\") || 40);\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n sendJson(res, 200, { ok: true, scope, id, kind, items: await listMemories({ scope, id, kind, limit }) });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const profile = await getPersonaGoalMemory(auth.uid);\n sendJson(res, 200, { ok: true, profile });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const profile = await setPersonaGoalMemory({\n uid: auth.uid,\n personaPatch: payload?.personaPatch || {},\n goals: Array.isArray(payload?.goals) ? payload.goals : null,\n preferencesPatch: payload?.preferencesPatch || {}\n });\n sendJson(res, 200, { ok: true, profile });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"profile_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/compact\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const out = await autoCompactMemories({ scope, id });\n sendJson(res, 200, { ok: true, scope, id, ...out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_compact_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/context\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const context = await getMemoryContext({\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n query: String(payload?.query || \"\"),\n limit: Number(payload?.limit || 80)\n });\n sendJson(res, 200, { ok: true, context });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_context_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/place-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const placeName = String(payload?.placeName || \"\").trim();\n if (!placeName) return sendJson(res, 400, { ok: false, error: \"placeName_required\" });\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const persona = String(payload?.waypointPersona || payload?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(placeName)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Place: ${placeName}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Give a concise conversational briefing about this place for a walking-route companion.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-place-brief\", placeName, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `PLACE_BRIEF:${placeName} => ${String(summary.reply || \"\").slice(0, 900)}`,\n tags: [\"place\", \"brief\", \"event-layer\"],\n importance: 0.62,\n meta: { placeName }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"place-brief\",\n text: `${placeName} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: {\n placeName,\n directive: summary.directive,\n lat: hit?.lat ? Number(hit.lat) : null,\n lon: hit?.lon ? Number(hit.lon) : null\n }\n });\n sendJson(res, 200, { ok: true, place: hit || null, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"place_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/route-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const waypoints = Array.isArray(payload?.waypoints) ? payload.waypoints.slice(0, 24) : [];\n const out = [];\n for (const wp of waypoints) {\n const name = String(wp?.name || \"\").trim();\n if (!name) continue;\n const persona = String(wp?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(name)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Waypoint: ${name}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Generate a concise spoken briefing for route waypoint.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-route-brief\", waypoint: name, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n out.push({\n name,\n persona,\n place: hit || null,\n reply: summary.reply,\n directive: summary.directive\n });\n }\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `ROUTE_BRIEF: waypoints=${out.length} ${out.map((x) => `${x.name}:${x.persona}`).join(\" | \")}`.slice(0, 1300),\n tags: [\"route\", \"waypoint\", \"brief\", \"event-layer\"],\n importance: 0.74,\n meta: { count: out.length }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"route-brief\",\n text: out.map((x) => `${x.name}:${String(x.reply || \"\").slice(0, 140)}`).join(\" | \").slice(0, 1400),\n roomId: \"studio-main\",\n meta: {\n waypointCount: out.length,\n waypoints: out.map((x) => ({\n name: x.name,\n persona: x.persona,\n lat: x.place?.lat ? Number(x.place.lat) : null,\n lon: x.place?.lon ? Number(x.place.lon) : null\n }))\n }\n });\n sendJson(res, 200, { ok: true, items: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"route_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/pdf-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const title = String(payload?.title || \"document\");\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const providedText = String(payload?.text || \"\");\n const url = String(payload?.url || \"\");\n let content = providedText;\n if (!content && url) content = await fetchTextFromUrl(url);\n if (!content) return sendJson(res, 400, { ok: false, error: \"text_or_supported_url_required\" });\n const summary = await queryRhizohLlm(\n {\n message: `Summarize this document and provide key points + practical actions.\\nTitle:${title}\\nContent:\\n${content.slice(0, 12000)}`,\n context: { source: \"event-layer-pdf-brief\", title },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `DOC_BRIEF:${title} => ${String(summary.reply || \"\").slice(0, 1000)}`,\n tags: [\"document\", \"brief\", \"library\", \"event-layer\"],\n importance: 0.7,\n meta: { title, url: url || null }\n });\n await addTranscript(auth.uid, {\n source: \"library-brief\",\n eventType: \"pdf-brief\",\n text: `${title} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { title }\n });\n sendJson(res, 200, { ok: true, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/pdf/upload\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req, 8 * 1024 * 1024);\n const job = await enqueuePdfJob({\n uid: auth.uid,\n fileName: payload?.fileName || \"document.pdf\",\n bytesBase64: payload?.contentBase64\n });\n sendJson(res, 200, { ok: true, job });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_upload_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/session\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createPublishSession(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session\",\n text: `Publish session armed: ${row.protocol} ${row.target}`.slice(0, 500),\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: row.id, bridge: row.bridge }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/studio/publish/sessions\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listPublishSessions(auth.uid, 80);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/health-check\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const protocol = String(payload?.protocol || \"WHIP\");\n const target = String(payload?.target || \"\");\n const health = await checkPublishTargetHealth(protocol, target);\n if (payload?.sessionId) {\n await updatePublishSession(auth.uid, String(payload.sessionId), {\n status: health.ok ? \"healthy\" : \"degraded\",\n reconnectPolicy: {\n enabled: true,\n backoffSec: [2, 5, 10, 20],\n nextRetryInSec: health.reconnectInSec || 0,\n maxRetries: 8,\n reason: health.reason\n }\n });\n }\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-health\",\n text: `${protocol} ${target} => ${health.ok ? \"healthy\" : \"degraded\"} (${health.reason})`,\n roomId: \"studio-main\",\n meta: { protocol, target, reconnect: !!health.shouldReconnect }\n });\n sendJson(res, 200, { ok: true, health });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_health_check_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/studio/publish/sessions/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updatePublishSession(auth.uid, id, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session-update\",\n text: `Session ${id} -> ${row.status || \"updated\"}`,\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: id, status: row.status, ingestUrl: row.ingestUrl || \"\" }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/transcripts\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/transcripts\"}`);\n const limit = Math.max(1, Math.min(200, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await listTranscripts(auth.uid, limit);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/transcripts\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await addTranscript(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"transcript_write_failed\" });\n }\n return;\n }\n\n /** E1: broadcast intent → GreenRoom capability → transcript + memory artifact */\n if (req.method === \"POST\" && req.url === \"/studio/capabilities/greenroom\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const traceId = String(payload?.traceId || `TRC-${Date.now().toString(36).toUpperCase()}`);\n const title = String(payload?.title || \"Castle Live Broadcast\").slice(0, 200);\n const intentRaw = String(payload?.intentRaw || \"\").slice(0, 800);\n const audienceEstimate = Math.max(0, Math.min(50_000_000, Number(payload?.audienceEstimate) || 1200));\n const roomId = String(payload?.roomId || \"greenroom-main\");\n const transcriptRow = await addTranscript(auth.uid, {\n source: \"rhizoh\",\n eventType: \"BROADCAST_ROUTED\",\n text: `GreenRoom · ${title}${intentRaw ? `\\nIntent: ${intentRaw}` : \"\"}`,\n roomId,\n meta: {\n ack: \"BROADCAST_ROUTED\",\n capability: \"greenroom\",\n intentType: \"broadcast\",\n traceId,\n audienceEstimate,\n artifact: {\n kind: \"broadcast_card\",\n title,\n traceId,\n roomId,\n createdAt: Date.now()\n }\n }\n });\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n text: `GREENROOM_BROADCAST:${title} trace=${traceId}`,\n tags: [\"broadcast\", \"greenroom\", \"artifact\", \"castle-library\"],\n importance: 0.72,\n kind: \"episodic\",\n meta: { traceId, capability: \"greenroom\", transcriptId: transcriptRow.id }\n });\n sendJson(res, 200, {\n ok: true,\n ack: \"BROADCAST_ROUTED\",\n traceId,\n transcript: transcriptRow,\n replayPath: `/replay/${encodeURIComponent(traceId)}`,\n sharePath: `/replay/${encodeURIComponent(traceId)}`\n });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"greenroom_capability_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/metadata/search\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/metadata/search\"}`);\n const q = String(url.searchParams.get(\"q\") || \"\");\n const limit = Math.max(1, Math.min(300, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await searchMetadata(auth.uid, q, limit);\n sendJson(res, 200, { ok: true, q, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerDevice(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, device: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"device_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const lat = Number(payload?.lat);\n const lon = Number(payload?.lon);\n const speed = Math.max(0, Number(payload?.speed || 0));\n const cfgCenterLat = Number(process.env.CASTLE_GEOFENCE_CENTER_LAT || 41.0082);\n const cfgCenterLon = Number(process.env.CASTLE_GEOFENCE_CENTER_LON || 28.9784);\n const cfgRadiusM = Math.max(100, Number(process.env.CASTLE_GEOFENCE_RADIUS_M || 55000));\n const cfgSpeedCap = Math.max(1, Number(process.env.CASTLE_SPEED_CAP_MPS || 35));\n const dist = Number.isFinite(lat) && Number.isFinite(lon) ? haversineMeters(cfgCenterLat, cfgCenterLon, lat, lon) : 0;\n const geofenceOk = !Number.isFinite(dist) ? true : dist <= cfgRadiusM;\n const speedOk = speed <= cfgSpeedCap;\n const failsafe = !(geofenceOk && speedOk);\n const safety = {\n geofenceOk,\n speedOk,\n failsafe,\n distanceFromCenterM: Number.isFinite(dist) ? dist : null,\n speedCapMps: cfgSpeedCap\n };\n const row = await appendTelemetry(auth.uid, deviceId, {\n lat,\n lon,\n speed,\n battery: payload?.battery,\n mode: payload?.mode || (failsafe ? \"FAILSAFE_HOLD\" : \"ACTIVE\"),\n raw: payload?.raw || {}\n });\n await updateDevice(auth.uid, deviceId, {\n status: failsafe ? \"failsafe\" : \"active\",\n lastTelemetryAt: row.ts,\n safetyState: safety\n });\n if (failsafe) {\n await addTranscript(auth.uid, {\n source: \"robotics-guardrails\",\n eventType: \"failsafe\",\n text: `Device ${deviceId} failsafe triggered geofenceOk=${geofenceOk} speedOk=${speedOk}`,\n roomId: \"studio-main\",\n meta: { deviceId, safety }\n });\n }\n sendJson(res, 200, { ok: true, telemetry: row, safety, command: failsafe ? \"FAILSAFE_HOLD\" : \"CONTINUE\" });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"telemetry_ingest_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listTelemetry(auth.uid, 150);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createAutomation(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"automation-scheduled\",\n text: `Automation scheduled: ${row.title} at ${new Date(row.scheduleAt).toLocaleString()}`,\n roomId: \"studio-main\",\n meta: { automationId: row.id, action: row.action, deviceId: row.deviceId }\n });\n sendJson(res, 200, { ok: true, automation: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"automation_create_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAutomations(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listDevices(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const devs = await listDevices(auth.uid, 500);\n const d = devs.find((x) => x.id === deviceId);\n if (!d) return sendJson(res, 404, { ok: false, error: \"device_not_found\" });\n const action = String(payload?.action || \"\");\n const caps = Array.isArray(d.capabilityProfile) ? d.capabilityProfile : [];\n if (action && caps.length && !caps.includes(action) && !caps.includes(\"*\")) {\n return sendJson(res, 400, { ok: false, error: \"unsupported_action_for_device\", capabilityProfile: caps });\n }\n const cmd = await enqueueCommand(auth.uid, {\n deviceId,\n action: action || \"NOOP\",\n params: payload?.params || {},\n adapter: payload?.adapter || d.adapter || \"websocket\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-command-queue\",\n eventType: \"command-queued\",\n text: `${cmd.deviceId} -> ${cmd.action}`,\n roomId: \"studio-main\",\n meta: { commandId: cmd.id, adapter: cmd.adapter }\n });\n sendJson(res, 200, { ok: true, command: cmd });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listCommands(auth.uid, 200);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/robotics/commands/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updateCommandStatus(auth.uid, id, payload?.status || \"sent\");\n sendJson(res, 200, { ok: true, command: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/rhizoh/bridge\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const result = await runRhizohBrainV2({\n message: String(payload?.message || \"robotics bridge command\"),\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n worldState: payload?.worldState || {},\n deviceState: payload?.deviceState || {},\n intent: \"robotics-mechanics\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-bridge\",\n eventType: \"robotics-command\",\n text: `Bridge -> ${String(result.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { directive: result.directive, agentId: payload?.agentId || \"\" }\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"robotics_bridge_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/planner/graph\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const graph = buildTaskActionGraph(String(payload?.task || \"\"), payload?.context || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"planner-graph\",\n text: `Planner graph built: ${graph.taskTitle} nodes=${graph.nodes.length}`,\n roomId: \"studio-main\",\n meta: { taskTitle: graph.taskTitle, nodeCount: graph.nodes.length }\n });\n sendJson(res, 200, { ok: true, graph });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"planner_graph_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerSocialChannel(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, channel: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_channel_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listSocialChannels(auth.uid, 100);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/broadcast\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const text = String(payload?.text || \"\").slice(0, 1200);\n const channels = await listSocialChannels(auth.uid, 100);\n const enabled = channels.filter((c) => c.status === \"enabled\");\n const deliveries = [];\n for (const c of enabled) {\n const out = await deliverSocialWithRetry(c, text);\n deliveries.push({ id: c.id, platform: c.platform, ...out });\n }\n const okCount = deliveries.filter((d) => d.ok).length;\n await addTranscript(auth.uid, {\n source: \"social-layer\",\n eventType: \"social-broadcast\",\n text: `Broadcast delivered ${okCount}/${enabled.length} channel(s): ${text.slice(0, 240)}`,\n roomId: \"studio-main\",\n meta: { deliveries }\n });\n sendJson(res, 200, { ok: true, sentChannels: okCount, totalChannels: enabled.length, deliveries });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_broadcast_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/ethics/programs\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, ...buildEthicsPrograms() });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands/dispatch\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const all = await listCommands(auth.uid, 200);\n const queued = all.filter((c) => String(c.status || \"queued\") === \"queued\").slice(0, 50);\n const devices = await listDevices(auth.uid, 500);\n const byId = new Map(devices.map((d) => [d.id, d]));\n const results = [];\n for (const cmd of queued) {\n const dev = byId.get(cmd.deviceId);\n if (!dev) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: \"device_not_found\" });\n continue;\n }\n try {\n const dispatched = await dispatchCommandToAdapter(dev, cmd);\n await updateCommandStatus(auth.uid, cmd.id, \"sent\");\n results.push({ id: cmd.id, ok: true, adapter: dispatched.adapter, via: dispatched.via });\n } catch (error) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: String(error?.message || \"dispatch_failed\") });\n }\n }\n await addTranscript(auth.uid, {\n source: \"robotics-command-dispatch\",\n eventType: \"command-dispatch-batch\",\n text: `Dispatch batch size=${results.length} ok=${results.filter((x) => x.ok).length}`,\n roomId: \"studio-main\",\n meta: { results: results.slice(0, 30) }\n });\n sendJson(res, 200, { ok: true, processed: results.length, results });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"dispatch_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/pdf/jobs/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n const job = await getPdfJob(auth.uid, id);\n if (!job) return sendJson(res, 404, { ok: false, error: \"pdf_job_not_found\" });\n sendJson(res, 200, { ok: true, job });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const row = await appendMemory({\n scope,\n id,\n text: payload?.text,\n tags: payload?.tags || [],\n importance: payload?.importance,\n kind: payload?.kind || \"episodic\",\n meta: { uid: auth.uid, ...(payload?.meta || {}) }\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.externalTruth) {\n try {\n if (process.env.RHIZOH_EXTERNAL_TRUTH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_truth:${ip}`, RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n sendJson(res, 200, buildRhizohExternalGroundTruthPayload(req));\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.productOutcome) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const rawBody = await readHttpBodyText(req, 64 * 1024);\n const body = rawBody.trim() ? safeJsonParse(rawBody) : null;\n if (!body || typeof body !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"invalid_json_payload\" });\n return;\n }\n const sigHdr = String(req.headers?.[\"x-rhizoh-outcome-signature\"] || \"\").trim();\n const hmacV = verifyRhizohOutcomeHmac(rawBody, sigHdr);\n if (!hmacV.ok) {\n sendJson(res, 401, { ok: false, error: hmacV.error || \"outcome_hmac_invalid\" });\n return;\n }\n const srcTokHdr = String(req.headers?.[\"x-rhizoh-outcome-source-token\"] || \"\").trim();\n const srcV = verifyRhizohOutcomeSourceToken(body.source, srcTokHdr);\n if (!srcV.ok) {\n sendJson(res, 401, { ok: false, error: srcV.error || \"outcome_source_token_invalid\" });\n return;\n }\n const subjectKey = String(body.subjectKey || \"\").trim().slice(0, 128);\n if (\n subjectKey &&\n !checkHttpRateLimit(\n `rhizoh_product_outcome_subj:${subjectKey}`,\n RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN,\n 60_000\n )\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_subject\" });\n return;\n }\n const result = ingestRhizohProductOutcomeHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n schemaVersion: \"1.1.0\",\n aggregate: result.aggregate,\n integrity: result.integrity\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"outcome_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.productOutcomeAggregate) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome_agg:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const u = new URL(String(req.url || \"/\"), \"http://localhost\");\n const cohortId = u.searchParams.get(\"cohortId\") || \"\";\n const decisionFingerprint = u.searchParams.get(\"decisionFingerprint\") || \"\";\n const limit = u.searchParams.get(\"limit\") || \"50\";\n const rows = listRhizohProductOutcomeAggregates({\n cohortId: cohortId || undefined,\n decisionFingerprint: decisionFingerprint || undefined,\n limit: Number(limit) || 50\n });\n sendJson(res, 200, { ok: true, schemaVersion: \"1.1.0\", rows });\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.externalLossBatch) {\n try {\n if (process.env.RHIZOH_EXTERNAL_LOSS_BATCH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_loss_batch:${ip}`, RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const result = ingestRhizohExternalLossBatchHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, { ok: true, accepted: result.accepted ?? 0 });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"batch_failed\") });\n }\n return;\n }\n\n function moderationAdminOk(req) {\n const expected = String(process.env.CASTLE_MODERATION_ADMIN_KEY || \"\").trim();\n if (!expected) return false;\n const got = String(req.headers[\"x-castle-moderation-key\"] || \"\").trim();\n return got.length > 0 && got === expected;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.opsAbuseReport) {\n try {\n const body = await readHttpJson(req, 32 * 1024);\n const auth = await resolveHttpUser(req);\n const entry = submitAbuseReportV0({\n reporterUid: auth.ok ? auth.uid : null,\n targetUid: body?.targetUid ?? null,\n category: body?.category,\n detail: body?.detail,\n traceId: body?.traceId ?? null\n });\n sendJson(res, 202, { ok: true, reportId: entry.id, status: entry.status });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsModerationQueue) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n queue: listModerationQueueV0(100),\n softBlocked: listSoftBlockedUsersV0()\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-block\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const out = softBlockUserV0(body?.uid, body?.reason);\n sendJson(res, out.ok ? 200 : 400, out);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-unblock\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n sendJson(res, 200, removeSoftBlockV0(body?.uid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsAgentSnapshots) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n const limit = Number(new URL(req.url || \"\", \"http://local\").searchParams.get(\"limit\") || 50);\n sendJson(res, 200, { ok: true, snapshots: listRecentAgentSnapshotsV0(limit) });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.academicObservatoryExport) {\n const auth = await resolveHttpUser(req);\n const out = handleAcademicObservatoryExportGetV0(req, auth);\n sendJson(res, out.status, out.body);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsHardeningStatus) {\n const auth = await resolveHttpUser(req);\n const principal = auth.ok ? `uid:${auth.uid}` : null;\n const gclSnap = await getGlobalCostLedgerSnapshotV0(principal);\n const economicStrategy = evaluateEconomicStrategyL3V0(gclSnap);\n const hardeningTenantId = resolveHardeningTenantIdV0(auth, req);\n const unifiedState = await buildUnifiedStateNarrativeV0({\n tenantId: hardeningTenantId ?? undefined,\n platformScope: !hardeningTenantId,\n principal\n });\n sendJson(res, 200, {\n ok: true,\n schema: \"rhizoh.ops.hardening_status.v0\",\n containment: readAgentContainmentConfigV0(),\n cost: readCostContainmentConfigV0(),\n gcl: {\n backend: await getGclBackendStatusV0(),\n snapshot: gclSnap\n },\n economicStrategyL3: economicStrategy,\n phasedRollout: await getPhasedRolloutStatsV0(),\n tier: readPhasedRolloutTierV0(),\n principalCost: auth.ok ? await getCostContainmentStatsV0(principal) : null,\n stressTaxonomy: verifyStressTaxonomyCoverageV0(),\n stressConflictResolution: verifyStressConflictResolutionV0(),\n resolutionStabilityEnvelope: verifyResolutionStabilityEnvelopeV0(),\n unifiedState: {\n tenantScope: {\n tenantId: unifiedState.tenantScope?.tenantId,\n scope: unifiedState.tenantScope?.scope,\n noGlobalDerivedState: unifiedState.tenantScope?.noGlobalDerivedState\n },\n narrativeFingerprint: unifiedState.narrativeFingerprint,\n screenshotScopeWatermark: unifiedState.screenshotScopeWatermark,\n interpretationSafetyContract: unifiedState.interpretationSafetyContract,\n interpretationUxContract: {\n schema: unifiedState.interpretationUxContract?.schema,\n layerOrder: unifiedState.interpretationUxContract?.layerOrder,\n narrativeNeverVisuallyDominant: unifiedState.interpretationUxContract?.narrativeNeverVisuallyDominant\n },\n governance: {\n narrativeIsNotDecisionLayer: unifiedState.governance.narrativeIsNotDecisionLayer,\n displayOrder: unifiedState.governance.displayOrder,\n trustPosture: unifiedState.governance.trustPosture,\n interpretationUxContract: unifiedState.governance.interpretationUxContract?.schema\n },\n humanOps: {\n decisionTier: unifiedState.humanOps?.humanDecisionScaling?.tier,\n decisionOwner: unifiedState.humanOps?.humanDecisionScaling?.decisionOwner,\n opsRouteId: unifiedState.humanOps?.humanDecisionOpsRunbook?.routing?.routeId,\n slaAckMinutes: unifiedState.humanOps?.humanDecisionOpsRunbook?.sla?.ackMinutes,\n uiDisclaimerRequired: unifiedState.humanOps?.narrativeUiSafety?.displayRules?.requireDisclaimerBanner,\n misreadHighResidual: unifiedState.humanOps?.misreadSimulation?.highResidualCount,\n propagationHighResidual: unifiedState.humanOps?.socialPropagationSimulation?.highResidualCount,\n worstWatermarkSurvivability:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.worstWatermarkSurvivability,\n dominantDistortionSource:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.dominantDistortionSource\n },\n culturalRisk: {\n perceptualRiskClass: unifiedState.culturalRisk?.perceptualRiskClass,\n trustFork: unifiedState.culturalRisk?.trustDynamics?.fork,\n trustDecayScore: unifiedState.culturalRisk?.trustDynamics?.scores?.trustDecay,\n mythologyScore: unifiedState.culturalRisk?.trustDynamics?.scores?.mythology,\n confidenceDetachmentExposure:\n unifiedState.culturalRisk?.trustDynamics?.axes?.confidenceDetachmentExposure\n },\n appliedSystemsLayer: {\n schema: unifiedState.appliedSystemsLayer?.schema,\n researchOutputType: unifiedState.appliedSystemsLayer?.researchGovernance?.outputType,\n roboticsBlockActuation: unifiedState.appliedSystemsLayer?.roboticsGrounding?.blockActuation,\n spiralRumorCount:\n unifiedState.appliedSystemsLayer?.spiralMMOGameKernel?.gameLayer?.rumors?.length\n },\n actionSemanticGovernance: {\n schema: unifiedState.actionSemanticGovernance?.schema,\n highRiskActionCount: unifiedState.actionSemanticGovernance?.highRiskActionCount,\n loopClosed: unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.loopClosed,\n authorityCollapseMitigated:\n unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.risks?.authorityCollapse\n ?.mitigated\n },\n actionContextResolution: {\n schema: unifiedState.actionContextResolution?.schema,\n contextFingerprint: unifiedState.actionContextResolution?.contextFingerprint?.fingerprintHash,\n domainCollision: unifiedState.actionContextResolution?.domainCollision?.detected,\n deployAgentSpiralBlocked:\n unifiedState.actionContextResolution?.actionOverloadingRisk?.deployAgentExample\n ?.spiral_mmo === \"blocked\",\n roboticsActuationEligible:\n unifiedState.actionContextResolution?.roboticsActuationCorrectness?.actuationEligible\n },\n epistemicCoherence: {\n schema: unifiedState.epistemicCoherence?.schema,\n verdict: unifiedState.epistemicCoherence?.systemCoherence?.verdict,\n coherenceScore: unifiedState.epistemicCoherence?.systemCoherence?.score,\n contradictionCount: unifiedState.epistemicCoherence?.crossLayerContradictions?.count,\n operatorDecision: unifiedState.epistemicCoherence?.uxCompression?.operatorDecision,\n headlineTr: unifiedState.epistemicCoherence?.uxCompression?.headline?.tr,\n falseSafetyWarning: Boolean(\n unifiedState.epistemicCoherence?.uxCompression?.falseSafetyWarning\n )\n },\n coherenceAuthorityBoundary: {\n schema: unifiedState.coherenceAuthorityBoundary?.schema,\n bindingValid: unifiedState.coherenceAuthorityBoundary?.eclAuthorityBinding?.valid,\n canExecute: unifiedState.coherenceAuthorityBoundary?.contract?.can_execute,\n mandatoryBannerTr:\n unifiedState.coherenceAuthorityBoundary?.disclaimers?.mandatoryBanner?.tr\n },\n decisionLatencyGovernance: {\n schema: unifiedState.decisionLatencyGovernance?.schema,\n latencyTier: unifiedState.decisionLatencyGovernance?.latencyTier,\n inflationScore: unifiedState.decisionLatencyGovernance?.latencyInflation?.score,\n fastPathEligible: unifiedState.decisionLatencyGovernance?.fastPath?.eligible,\n primaryActionId: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.primaryActionId,\n ackSlaMinutes: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.ackSlaMinutes,\n certaintyCap: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.certaintyCap,\n uncertaintyItems:\n unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.uncertaintyEnvelope?.itemCount\n },\n decisionPacketUncertaintyBoundary: {\n schema: unifiedState.decisionPacketUncertaintyBoundary?.schema,\n invariantValid: unifiedState.decisionPacketUncertaintyBoundary?.invariantCheck?.valid,\n centralInvariant: unifiedState.decisionPacketUncertaintyBoundary?.centralInvariant\n },\n epistemicDecisionPacing: {\n schema: unifiedState.epistemicDecisionPacing?.schema,\n queueSaturationTier:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation?.tier,\n operatorProcessingLatency:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation\n ?.operatorProcessingLatency,\n postureWindowCount:\n unifiedState.epistemicDecisionPacing?.temporaryStaticPostureWindows\n ?.temporaryStaticPostureWindows?.windowCount,\n executionBoundariesValid:\n unifiedState.epistemicDecisionPacing?.executionBoundaries?.valid,\n grantsExecutionApproval: unifiedState.epistemicDecisionPacing?.grantsExecutionApproval\n },\n epistemicTemporalCoherence: {\n schema: unifiedState.epistemicTemporalCoherence?.schema,\n allInvariantsValid: unifiedState.epistemicTemporalCoherence?.allInvariantsValid,\n temporalAlignmentValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.temporalAlignment?.valid,\n crossWindowGuardValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard?.valid,\n delayedTruthValid: unifiedState.epistemicTemporalCoherence?.invariants?.delayedTruthRule?.valid,\n reconciliationRequired:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard\n ?.reconciliation?.required\n },\n realityDriftObserver: {\n schema: unifiedState.realityDriftObserver?.schema,\n driftActive: unifiedState.realityDriftObserver?.driftObserverSummary?.active,\n misapprehensionShapeCount:\n unifiedState.realityDriftObserver?.misapprehensionShapeCatalog?.shapeCount,\n criticalShapeCount:\n unifiedState.realityDriftObserver?.driftObserverSummary?.criticalShapeCount,\n roboticsMismatchActive:\n unifiedState.realityDriftObserver?.roboticsFeedbackMismatch?.active\n },\n driftCausality: {\n schema: unifiedState.driftCausality?.schema,\n shapesWithCausalExplanation: unifiedState.driftCausality?.rdolLinkage?.shapesWithCausalExplanation,\n activeDomainCount: unifiedState.driftCausality?.causalitySummary?.activeDomainCount,\n propagationCausalityActive: unifiedState.driftCausality?.causalDomains?.propagation?.active\n },\n productReadiness: unifiedState.productReadiness,\n stateLayers: {\n raw: unifiedState.stateLayers.raw,\n derived: {\n systemState: unifiedState.stateLayers.derived.systemState,\n narrative: unifiedState.stateLayers.derived.narrative,\n validation: unifiedState.stateLayers.derived.validation\n },\n policy: {\n binding: unifiedState.stateLayers.policy.binding,\n decisionOwner: unifiedState.stateLayers.policy.decisionOwner,\n prohibitedActions: unifiedState.stateLayers.policy.prohibitedActions\n }\n },\n systemState: unifiedState.systemState,\n interpretation: unifiedState.interpretation,\n validation: {\n trustPosture: unifiedState.validation.trustPosture,\n adjustedConfidence: unifiedState.validation.adjustedConfidence,\n agreement: unifiedState.validation.confidenceDecomposition.agreement,\n divergenceScore: unifiedState.validation.divergence.divergenceScore,\n validated: unifiedState.validation.divergence.validated,\n uncertaintyTags: unifiedState.validation.uncertainty.tags.map((t) => t.id),\n flags: unifiedState.validation.divergence.flags.map((f) => f.id)\n },\n compression: unifiedState.compression\n }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 6 * 1024 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (\n !checkHttpRateLimit(`rhizoh_voice_transcribe:${rlKey}`, RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN, 60_000)\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const voiceEnv = rhizohVoiceTranscribeEnvV3();\n if (!voiceEnv.any) {\n sendJson(res, 503, {\n ok: false,\n error: \"voice_asr_not_configured\",\n env: voiceEnv\n });\n return;\n }\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n const result = await runRhizohVoiceTranscribeV3(\n applyLanguagePropagationToVoiceBodyV1(payload, langProp)\n );\n sendJson(res, result.ok ? 200 : 422, {\n ...result,\n traceId: String(payload?.traceId || \"\"),\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n atMs: Date.now()\n });\n } catch (error) {\n sendJson(res, 500, {\n ok: false,\n error: \"voice_transcribe_failed\",\n detail: String(error?.message || error),\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(\"/rhizoh/llm/task/\")) {\n const taskId = pathname.slice(\"/rhizoh/llm/task/\".length).split(\"/\")[0];\n const snap = getLlmWorkerTaskSnapshotV0(taskId);\n if (!snap) {\n sendJson(res, 404, { ok: false, error: \"llm_task_not_found\", taskId, status: \"missing\" });\n return;\n }\n if (snap.status === \"processing\") {\n sendJson(res, 202, { ok: true, taskId: snap.taskId, status: \"processing\", createdAt: snap.createdAt });\n return;\n }\n sendJson(res, snap.httpStatus || 200, snap.body || { ok: false, error: \"llm_task_empty\", taskId: snap.taskId });\n return;\n }\n\n if (req.method === \"POST\" && req.url === rhizohRuntime.routes.rhizohLlm) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 128 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n logRhizohHealth(\"gateway_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: String(payload?.traceId || \"\"),\n auth: auth.ok ? \"ok\" : \"anon\",\n ip\n });\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (!checkHttpRateLimit(`rhizoh_llm:${rlKey}`, RL_RHIZOH_LLM_PER_MIN, 60_000)) {\n const rlTaxonomy = classifyStressResponseV0({ code: \"rate_limit_exceeded\" });\n return sendJson(res, 429, {\n ok: false,\n error: \"rate_limit_exceeded\",\n reply: rlTaxonomy.userMessageTr || \"İstek sınırı aşıldı. Kısa süre sonra tekrar deneyin.\",\n stressClass: rlTaxonomy.stressClass,\n responseAction: rlTaxonomy.responseAction,\n stressMatrix: rlTaxonomy.matrix,\n stressInterpretable: rlTaxonomy.interpretable,\n stressConfidence: rlTaxonomy.stressConfidence,\n actionConfidence: rlTaxonomy.actionConfidence,\n actionSoftened: rlTaxonomy.actionSoftened,\n responseActionStrict: rlTaxonomy.responseActionStrict,\n actionInterpretable: rlTaxonomy.actionInterpretable,\n conflictResolution: rlTaxonomy.conflictResolution,\n directive: \"NONE\"\n });\n }\n\n const requireExplicit = process.env.CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE === \"1\";\n const keyMode = normalizeClientLlmKeySource(payload?.llmKeySource ?? payload?.keySource);\n if (keyMode === null) {\n return sendJson(res, 400, {\n ok: false,\n error: \"invalid_llm_key_source\",\n reply: \"llmKeySource geçersiz. Kullanın: env | user_connection | auto\",\n directive: \"NONE\"\n });\n }\n if (requireExplicit && keyMode === \"auto\") {\n return sendJson(res, 400, {\n ok: false,\n error: \"llm_key_source_required\",\n reply: \"Sunucu CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE=1: istekte llmKeySource (env veya user_connection) zorunlu.\",\n directive: \"NONE\"\n });\n }\n\n const conn = auth.ok ? await resolveConnection(auth.uid, String(payload?.connectionId || \"\")) : null;\n\n if (keyMode === \"user_connection\") {\n if (!auth.ok) {\n return sendJson(res, 401, {\n ok: false,\n error: \"llm_auth_required_for_user_connection\",\n reply: \"Kayıtlı LLM bağlantısı için giriş gerekli.\",\n directive: \"NONE\"\n });\n }\n if (!conn?.apiKey) {\n return sendJson(res, 400, {\n ok: false,\n error: \"user_llm_connection_required\",\n reply: \"Geçerli bir LLM bağlantısı yok; Studio’da bağlantı ekleyin veya llmKeySource=env kullanın.\",\n directive: \"NONE\"\n });\n }\n }\n\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n let safePayload = applyLanguagePropagationToLlmPayloadV1({ ...(payload || {}) }, langProp);\n const trimOut = trimRhizohLlmGatewayPayloadV1(safePayload);\n if (trimOut.meta.trimmed) {\n logRhizohHealth(\"llm_payload_trimmed\", {\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n bytesBefore: trimOut.meta.bytesBefore,\n bytesAfter: trimOut.meta.bytesAfter\n });\n }\n safePayload = trimOut.payload;\n delete safePayload.apiKey;\n delete safePayload.llmKeySource;\n delete safePayload.keySource;\n\n let resolvedProvider = payload?.provider;\n let resolvedModel = payload?.model;\n let connApiKey = \"\";\n\n if (keyMode === \"user_connection\") {\n resolvedProvider = conn.provider;\n resolvedModel = conn.model;\n connApiKey = conn.apiKey || \"\";\n } else if (keyMode === \"env\") {\n connApiKey = \"\";\n } else {\n resolvedProvider = conn?.provider || payload?.provider;\n resolvedModel = conn?.model || payload?.model;\n connApiKey = conn?.apiKey || \"\";\n }\n\n const turnInput = {\n safePayload,\n auth,\n keyMode,\n conn,\n resolvedProvider,\n resolvedModel,\n connApiKey\n };\n const turnMeta = {\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n langEcho: buildRhizohLanguagePropagationEchoV1(langProp)\n };\n\n if (isLlmWorkerEnabledV0() && isLlmWorkerAsyncEnabledV0()) {\n const taskId = enqueueLlmWorkerTaskAsyncV0(turnInput, turnMeta);\n logRhizohHealth(\"llm_worker_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n taskId,\n traceId: String(payload?.traceId || taskId),\n keyMode,\n async: true\n });\n sendJson(res, 202, {\n ok: true,\n taskId,\n status: \"processing\",\n traceId: String(payload?.traceId || taskId),\n pollPath: `/rhizoh/llm/task/${taskId}`,\n ...turnMeta.langEcho\n });\n return;\n }\n\n const { result, traceId, turnLatencyMs, spinePhases, sampledTrace } = isLlmWorkerEnabledV0()\n ? await runLlmWorkerTaskV0(turnInput, turnMeta)\n : await rhizohGatewayTurn(turnInput);\n logRhizohHealth(\"llm_response\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId,\n turnLatencyMs: Number(turnLatencyMs || 0),\n keyMode\n });\n sendJson(res, 200, {\n ...result,\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n traceId,\n turnLatencyMs,\n ...(spinePhases ? { spinePhases } : {}),\n ...(sampledTrace !== undefined ? { sampledTrace } : {})\n });\n } catch (error) {\n const msg = String(error?.message || \"\");\n const code = error?.code || \"\";\n let status = 500;\n if (msg === \"payload_too_large\") status = 413;\n else if (msg === \"rate_limit_exceeded\") status = 429;\n else if (\n code === \"cost_hard_limit\" ||\n code === \"phased_rollout_capacity\" ||\n error?.containment === true ||\n String(code).startsWith(\"agent_\")\n ) {\n status = 429;\n } else if (code === \"user_soft_blocked\" || code === \"prompt_abuse_detected\") status = 403;\n else if ([\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(code) || msg.includes(\"missing_api_key_for_\"))\n status = 400;\n let rhizohFailureKind = \"provider_error\";\n if (msg === \"rate_limit_exceeded\" || status === 429) rhizohFailureKind = \"rate_limit\";\n else if (status === 403) rhizohFailureKind = \"policy_block\";\n else if (status === 400 || status === 413) rhizohFailureKind = \"client_config\";\n else if (msg.startsWith(\"provider_http_\")) {\n const st = Number(msg.replace(\"provider_http_\", \"\"));\n if (st === 408 || st === 504) rhizohFailureKind = \"timeout\";\n else rhizohFailureKind = \"provider_error\";\n } else if (/timeout|timed out|ETIMEDOUT/i.test(msg)) rhizohFailureKind = \"timeout\";\n\n const providerHttpMatch = msg.match(/^provider_http_(\\d{3})$/);\n const stressTaxonomy =\n error?.stressClass != null\n ? {\n stressClass: error.stressClass,\n responseAction: error.responseAction,\n stressMatrix: error.stressMatrix,\n stressInterpretable: error.stressInterpretable !== false,\n stressConfidence: error.stressConfidence,\n actionConfidence: error.actionConfidence,\n actionSoftened: error.actionSoftened,\n responseActionStrict: error.responseActionStrict,\n actionInterpretable: error.actionInterpretable,\n conflictResolution: error.conflictResolution,\n stressSecondary: error.stressSecondary,\n responseActions: error.responseActions\n }\n : (() => {\n const t = classifyStressResponseV0({\n code: code || msg,\n rhizohFailureKind,\n providerHttpStatus: providerHttpMatch\n ? Number(providerHttpMatch[1])\n : null\n });\n return {\n stressClass: t.stressClass,\n responseAction: t.responseAction,\n stressMatrix: t.matrix,\n stressInterpretable: t.interpretable,\n stressConfidence: t.stressConfidence,\n actionConfidence: t.actionConfidence,\n actionSoftened: t.actionSoftened,\n responseActionStrict: t.responseActionStrict,\n actionInterpretable: t.actionInterpretable,\n conflictResolution: t.conflictResolution,\n stressSecondary: t.stressSecondary,\n responseActions: t.responseActions\n };\n })();\n sendJson(res, status, {\n ok: false,\n rhizohFailureKind,\n ...(providerHttpMatch ? { providerHttpStatus: Number(providerHttpMatch[1]) } : {}),\n error: code || msg || \"rhizoh_llm_failed\",\n detail: msg || code || undefined,\n reply:\n error?.reply ||\n (status === 500 ? \"Rhizoh bağlantısı geçici olarak kesildi.\" : msg),\n directive: error?.directive || \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n ...stressTaxonomy\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicSeal) {\n try {\n if (!EPISTEMIC_SEAL_SECRET) {\n sendJson(res, 503, { ok: false, error: \"epistemic_seal_disabled\" });\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`epistemic_seal:${ip}`, RL_EPISTEMIC_SEAL_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const truth = body.truth_contract;\n if (!truth || typeof truth !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"truth_contract_required\" });\n return;\n }\n const sealBody = {\n truth_contract: truth,\n runtime_hash: String(body.runtime_hash || \"\"),\n model_route: {\n provider: body.model_route?.provider ?? null,\n model: body.model_route?.model ?? null\n },\n memory_digest: String(body.memory_digest || \"\"),\n world_snapshot_hash: String(body.world_snapshot_hash || \"\"),\n timestamp: Number(body.timestamp) || Date.now()\n };\n const canonical = canonicalEpistemicSealString(sealBody);\n const { hash, signature } = hashAndSignEpistemicSeal(canonical, EPISTEMIC_SEAL_SECRET);\n recordGenesisEpistemicSealIssued(hash);\n const attestation = {\n schema: EPISTEMIC_SEAL_SCHEMA,\n algorithm: \"SHA-256+HMAC-SHA256\",\n keyHint: process.env.CASTLE_EPISTEMIC_SEAL_SECRET ? \"CASTLE_EPISTEMIC_SEAL_SECRET\" : \"CASTLE_GATEWAY_TOKEN\",\n issuedAt: Date.now(),\n gateway: { service: \"castle-gateway\", listenPort: PORT }\n };\n sendJson(res, 200, {\n ok: true,\n hash,\n signature,\n attestation,\n canonicalBytes: Buffer.byteLength(canonical, \"utf8\")\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"seal_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicLogsBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`epistemic_logs:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 120) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const persisted = await persistEpistemicLedgerBatch(auth.uid, entries);\n recordGenesisEpistemicLedgerPersisted(persisted.persisted);\n // Internal cognition layer: forecast stays gateway/ledger-only (no client exposure).\n try {\n await persistEpistemicForecastBatch(auth.uid, persisted.normalized || []);\n } catch {\n /* forecast failures must not break primary ledger ingest */\n }\n sendJson(res, 200, {\n ok: true,\n persisted: persisted.persisted,\n mode: persisted.mode,\n latest: persisted.normalized?.[persisted.normalized.length - 1] || null\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"epistemic_log_ingest_failed\") });\n }\n return;\n }\n\n sendJson(res, 404, { ok: false, error: \"not_found\" });\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\nspiralWssForGenesis = wss;\nconst orchestrator = createOrchestrator();\nconst clientStats = new Map();\nlet broadcasterClientId = null;\nconst spiralState = {\n rooms: [{ id: \"room-default\", name: \"Spiral Default Arena\" }],\n activeByClient: new Map(),\n characters: new Map() // id -> {id,name,role,roomId,source}\n};\n\n/** Castle social rooms — keyed by `castleRoomKey` from {@link WS_MESSAGE.CASTLE_SOCIAL_PULSE}. */\nconst castleSocialRoomState = new Map();\nconst CASTLE_SOCIAL_MEMBER_TTL_MS = 90_000;\nconst CASTLE_SOCIAL_MAX_PULSE_BYTES = 8192;\nconst RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES = 5 * 1024 * 1024;\nconst RHIZOH_VOICE_LIVE_MAX_CHUNKS = 96;\n\n/**\n * Voice Engine v3 live media lane.\n * Client streams MediaRecorder chunks over the existing gateway WS; STOP reuses the\n * gateway ASR runner so orchestration remains the single authority downstream.\n */\nconst rhizohVoiceLiveSessions = new WeakMap();\n\nfunction appendRhizohVoiceLiveEventV0(session, type, payloadRefSeed = \"\") {\n if (!session) return null;\n session.eventSeq = (Number(session.eventSeq) || 0) + 1;\n const event = createRhizohImmutableEventV0({\n type,\n sessionId: session.sessionId,\n traceId: session.traceId,\n actorSeed: session.sessionId || session.traceId,\n eventSeq: session.eventSeq,\n payloadRef: createRhizohPayloadRefV0(payloadRefSeed || `${session.sessionId}:${type}:${session.eventSeq}`)\n });\n session.events = [...(session.events || []), event].slice(-64);\n return event;\n}\n\nfunction sendRhizohVoiceLiveEnvelopeV0(socket, type, payload) {\n try {\n if (socket?.readyState === 1) socket.send(JSON.stringify(createEnvelope(type, payload)));\n } catch {\n /* noop */\n }\n}\n\nfunction decodeRhizohVoiceLiveChunkV0(payload = {}) {\n const b64 = String(payload.audioBase64 || \"\").trim();\n if (!b64) return { ok: false, error: \"audio_base64_required\" };\n try {\n const buf = Buffer.from(b64, \"base64\");\n if (!buf.length) return { ok: false, error: \"audio_decode_empty\" };\n return { ok: true, buffer: buf };\n } catch {\n return { ok: false, error: \"audio_base64_invalid\" };\n }\n}\n\nfunction handleRhizohVoiceLiveStartV0(socket, payload = {}) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const sessionId = String(p.sessionId || `live_${Date.now().toString(36)}`).slice(0, 96);\n rhizohVoiceLiveSessions.set(socket, {\n sessionId,\n traceId: String(p.traceId || \"\").slice(0, 128),\n mimeType: String(p.mimeType || \"audio/webm\").slice(0, 80),\n languageCode: String(p.languageCode || \"tr-TR\").slice(0, 32),\n path: String(p.path || \"both\").slice(0, 24),\n chunks: [],\n chunkIndexes: [],\n totalBytes: 0,\n nextChunkIndex: 1,\n eventSeq: 0,\n events: [],\n startedAtMs: Date.now()\n });\n appendRhizohVoiceLiveEventV0(rhizohVoiceLiveSessions.get(socket), \"VOICE_LIVE_SESSION_STARTED\", `${sessionId}:live_session`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_START, {\n ok: true,\n sessionId,\n schema: \"castle.rhizoh.voice_live.gateway_lane.v0\"\n });\n}\n\nfunction handleRhizohVoiceLiveChunkV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const sequence = validateRhizohVoiceLiveChunkPayloadV0(payload, {\n expectedChunkIndex: session.nextChunkIndex\n });\n if (!sequence.ok) {\n rhizohVoiceLiveSessions.delete(socket);\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:chunk_reject`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: sequence.error,\n expectedChunkIndex: sequence.expectedChunkIndex,\n chunkIndex: sequence.chunkIndex\n });\n return;\n }\n const decoded = decodeRhizohVoiceLiveChunkV0(payload);\n if (!decoded.ok) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: decoded.error\n });\n return;\n }\n if (\n session.chunks.length >= RHIZOH_VOICE_LIVE_MAX_CHUNKS ||\n session.totalBytes + decoded.buffer.length > RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES\n ) {\n rhizohVoiceLiveSessions.delete(socket);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_audio_too_large\"\n });\n return;\n }\n session.chunks.push(decoded.buffer);\n session.chunkIndexes.push(sequence.chunkIndex);\n session.totalBytes += decoded.buffer.length;\n session.nextChunkIndex += 1;\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_CHUNK_ACCEPTED\", `${session.sessionId}:chunk:${sequence.chunkIndex}`);\n}\n\nfunction handleRhizohVoiceLiveStopV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n rhizohVoiceLiveSessions.delete(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const lastChunkIndex = Number(payload?.lastChunkIndex);\n if (Number.isInteger(lastChunkIndex) && lastChunkIndex !== session.chunks.length) {\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:stop_mismatch`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_chunk_count_mismatch\",\n expectedChunkCount: lastChunkIndex,\n receivedChunkCount: session.chunks.length\n });\n return;\n }\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_STOP_RECEIVED\", `${session.sessionId}:stop:${session.chunks.length}`);\n const audio = Buffer.concat(session.chunks);\n if (!audio.length) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"audio_empty\"\n });\n return;\n }\n runRhizohVoiceTranscribeV3({\n path: String(payload?.path || session.path || \"both\"),\n audioBase64: audio.toString(\"base64\"),\n mimeType: String(payload?.mimeType || session.mimeType || \"audio/webm\"),\n languageCode: String(payload?.languageCode || session.languageCode || \"tr-TR\"),\n traceId: String(payload?.traceId || session.traceId || \"\"),\n sessionId: session.sessionId,\n transport: \"gateway_ws_live\"\n })\n .then((result) => {\n appendRhizohVoiceLiveEventV0(session, result?.ok ? \"VOICE_TRANSCRIBE_FINAL\" : \"VOICE_TRANSCRIBE_FAILED\", `${session.sessionId}:final`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_FINAL, {\n ...result,\n sessionId: session.sessionId,\n transportPath: \"gateway_ws_live\",\n bytes: audio.length,\n chunkCount: session.chunks.length,\n chunkSequence: Object.freeze({\n ok: true,\n firstChunkIndex: session.chunkIndexes[0] || null,\n lastChunkIndex: session.chunkIndexes[session.chunkIndexes.length - 1] || null,\n receivedChunkCount: session.chunks.length\n }),\n eventTrace: Object.freeze((session.events || []).slice(-16)),\n recordedMs: Math.max(0, Date.now() - session.startedAtMs)\n });\n })\n .catch((e) => {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_transcribe_failed\",\n detail: String(e?.message || e)\n });\n });\n}\n\nfunction removeCastleSocialClientFromAllRooms(clientId) {\n const cid = String(clientId || \"\");\n if (!cid) return;\n for (const room of castleSocialRoomState.values()) {\n room.members.delete(cid);\n }\n}\n\nfunction buildCastleSocialRoomPayload(roomKey) {\n const room = castleSocialRoomState.get(roomKey);\n if (!room) return null;\n const now = Date.now();\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const roster = [...room.members.values()].map((m) => ({\n userId: m.userId,\n lastMs: m.lastMs,\n ...m.sp\n }));\n const peerFeeds =\n room.walPeers && room.walPeers.size\n ? [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }))\n : [];\n return { castleRoomKey: roomKey, seq: room.seq, roster, peerFeeds, ts: now };\n}\n\nconst CASTLE_WAL_PEER_MAX_FEED_BYTES = 48 * 1024;\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {{ ok: boolean, code?: string, reason?: string }} gate\n */\nfunction rejectWalPeerIngressV0(socket, gate) {\n const code = gate.code || \"wal_peer_rejected\";\n recordWalPeerRejectV0(code);\n recordSubstrateOtelEventV0(\"wal_peer_reject\", { code, reason: String(gate.reason || \"\").slice(0, 120) });\n socket.send(\n JSON.stringify(\n createEnvelope(WS_MESSAGE.ERROR, {\n error: gate.reason || \"WAL peer ingress rejected.\",\n code\n })\n )\n );\n}\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {Record<string, unknown> | null | undefined} walPeerFeed\n * @returns {boolean}\n */\nfunction gateWalPeerIngressV0(socket, walPeerFeed) {\n const authGate = assertWalPeerSocketAuthorityV0(socket);\n if (!authGate.ok) {\n rejectWalPeerIngressV0(socket, authGate);\n return false;\n }\n const sigGate = validateWalPeerFeedSignatureV0(\n walPeerFeed && typeof walPeerFeed === \"object\" ? walPeerFeed : {}\n );\n if (!sigGate.ok) {\n rejectWalPeerIngressV0(socket, sigGate);\n return false;\n }\n return true;\n}\n\nfunction handleCastleWalPeerFeed(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_WAL_PEER_MAX_FEED_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_WAL_PEER_FEED too large.\" })));\n return;\n }\n const walPeerFeed = p.walPeerFeed && typeof p.walPeerFeed === \"object\" ? p.walPeerFeed : {};\n if (!gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const castleRoomKey = String(p.castleRoomKey || \"wal:convergence\").slice(0, 64);\n const castleId = String(p.castleId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId,\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: room.members.get(socket.clientId)?.sp ?? {}\n });\n const peerFeeds = [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }));\n const body = {\n castleRoomKey,\n seq: room.seq,\n peerFeeds,\n ts: now\n };\n const env = createEnvelope(WS_MESSAGE.CASTLE_WAL_PEER_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys(), ...room.walPeers.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction handleCastleSocialPulse(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_SOCIAL_MAX_PULSE_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_SOCIAL_PULSE too large.\" })));\n return;\n }\n const castleRoomKey = String(p.castleRoomKey || \"default\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n const sp = p.socialPulse && typeof p.socialPulse === \"object\" ? p.socialPulse : {};\n const walPeerFeed =\n p.walPeerFeed && typeof p.walPeerFeed === \"object\"\n ? p.walPeerFeed\n : sp.walPeerFeed && typeof sp.walPeerFeed === \"object\"\n ? sp.walPeerFeed\n : null;\n if (walPeerFeed && !gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const spNext = walPeerFeed ? { ...sp, walPeerFeed } : { ...sp };\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: spNext\n });\n if (walPeerFeed) {\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId: String(p.castleId || userId).slice(0, 64),\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n }\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const body = buildCastleSocialRoomPayload(castleRoomKey);\n if (!body) return;\n const env = createEnvelope(WS_MESSAGE.CASTLE_SOCIAL_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcast(msgObj) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcastState() {\n broadcast(\n createEnvelope(WS_MESSAGE.BROADCAST_STATE, {\n broadcasterClientId,\n viewers: [...wss.clients].filter((c) => c.readyState === 1).length\n })\n );\n}\n\nfunction currentSpiralStatePayload() {\n return {\n rooms: spiralState.rooms,\n characters: [...spiralState.characters.values()]\n };\n}\n\nfunction broadcastSpiralState() {\n broadcast(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload()));\n}\n\nfunction ensureRoom(roomId, fallbackName = null) {\n if (!roomId) return spiralState.rooms[0].id;\n const exists = spiralState.rooms.find((r) => r.id === roomId);\n if (exists) return exists.id;\n const created = { id: roomId, name: fallbackName || `Spiral-${roomId.slice(-4)}` };\n spiralState.rooms.push(created);\n return created.id;\n}\n\nfunction syncSpiralCharactersWithWorld(snapshot) {\n const agents = snapshot?.agents || [];\n for (const a of agents) {\n if (!spiralState.characters.has(a.id)) {\n spiralState.characters.set(a.id, {\n id: a.id,\n name: a.id,\n role: \"agent\",\n roomId: spiralState.rooms[0].id,\n source: \"world\"\n });\n }\n }\n}\n\nfunction broadcastExcept(msgObj, exceptSocket) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client !== exceptSocket && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction validateInputFrame(payload, socket) {\n const stats = clientStats.get(socket.clientId) || { lastFrame: 0, windowStart: Date.now(), msgCount: 0 };\n const now = Date.now();\n if (now - stats.windowStart > 5000) {\n stats.windowStart = now;\n stats.msgCount = 0;\n }\n stats.msgCount += 1;\n clientStats.set(socket.clientId, stats);\n if (stats.msgCount > 120) return \"Rate limit exceeded.\";\n\n if (!payload || !Number.isInteger(payload.frame)) return \"frame must be integer.\";\n if (payload.frame <= stats.lastFrame) return \"frame must be monotonic.\";\n const commands = Array.isArray(payload.commands) ? payload.commands : [];\n if (commands.length > 8) return \"Too many commands in one frame.\";\n for (const cmd of commands) {\n if (!cmd || typeof cmd.command !== \"string\") return \"Invalid command format.\";\n if (\n cmd.command !== \"SPAWN_AGENT\" &&\n cmd.command !== \"LIST_CASTLES\" &&\n cmd.command !== \"SPAWN_ENTITY\" &&\n cmd.command !== \"CREATE_CASTLE\"\n ) return \"Unsupported command.\";\n }\n stats.lastFrame = payload.frame;\n clientStats.set(socket.clientId, stats);\n return null;\n}\n\nfunction validateSignalPayload(payload) {\n if (!payload || typeof payload !== \"object\") return \"Invalid signaling payload.\";\n const signalType = payload.signalType;\n if (![\"OFFER\", \"ANSWER\", \"ICE\"].includes(signalType)) return \"Invalid signal type.\";\n if (typeof payload.to !== \"string\" || payload.to.length < 3) return \"Invalid signal destination.\";\n if (signalType !== \"ICE\" && JSON.stringify(payload.sdp || {}).length > 16 * 1024) return \"SDP too large.\";\n if (signalType === \"ICE\" && JSON.stringify(payload.candidate || {}).length > 6 * 1024) return \"ICE too large.\";\n return null;\n}\n\nfunction sanitizeRawMessage(raw) {\n if (!raw) return null;\n const text = raw.toString();\n if (Buffer.byteLength(text, \"utf8\") > MAX_MESSAGE_BYTES) return null;\n return text;\n}\n\nwss.on(\"connection\", async (socket, req) => {\n const origin = req?.headers?.origin || \"\";\n if (!httpCors.isWebSocketOriginAllowed(origin)) {\n socket.close(1008, \"Origin not allowed\");\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const path = req?.url || \"\";\n const query = new URL(`ws://localhost${path}`).searchParams;\n const token = query.get(\"token\");\n if (!token || token !== REQUIRED_GATEWAY_TOKEN) {\n socket.close(1008, \"Invalid gateway token\");\n return;\n }\n }\n if (REQUIRE_AUTH) {\n const result = await verifyClientToken(req);\n if (!result.ok) {\n const isLocal = origin.includes(\"localhost\") || origin.includes(\"127.0.0.1\") || !origin;\n if (!(ALLOW_DEV_ANON && isLocal)) {\n socket.close(1008, result.reason);\n return;\n }\n socket.auth = { ok: true, kind: \"dev-anon\", user: { uid: \"dev-anon\" } };\n } else {\n socket.auth = result;\n }\n }\n\n socket.clientId = `c-${Math.random().toString(36).slice(2, 8)}`;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.HELLO, { message: \"Castle Gateway online.\", clientId: socket.clientId })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.WORLD_SNAPSHOT, orchestrator.worldSnapshot())));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.BROADCAST_STATE, { broadcasterClientId, viewers: [...wss.clients].filter((c) => c.readyState === 1).length })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload())));\n broadcastExcept(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }), socket);\n\n socket.on(\"message\", (raw) => {\n const safeRaw = sanitizeRawMessage(raw);\n if (!safeRaw) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Message too large or invalid.\" })));\n return;\n }\n const parsed = safeJsonParse(safeRaw);\n if (!parsed?.type) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid message.\" })));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND_TEXT) {\n const canonical = parseTextToCommand(parsed.payload?.text);\n if (!canonical) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Unknown command text.\" })));\n return;\n }\n const result = orchestrator.applyCommand(canonical);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND) {\n const result = orchestrator.applyCommand(parsed.payload);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n }\n\n if (parsed.type === WS_MESSAGE.INPUT_FRAME) {\n const err = validateInputFrame(parsed.payload, socket);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const frame = parsed.payload.frame;\n const commands = Array.isArray(parsed.payload?.commands) ? parsed.payload.commands : [];\n orchestrator.queueInputFrame(socket.clientId, frame, commands);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, queued: true, frame })));\n }\n\n if (parsed.type === WS_MESSAGE.SIGNAL) {\n const payload = parsed.payload || {};\n const err = validateSignalPayload(payload);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const signalType = payload.signalType;\n const to = payload.to;\n for (const client of wss.clients) {\n if (client.clientId === to && client.readyState === 1) {\n client.send(JSON.stringify(createEnvelope(WS_MESSAGE.SIGNAL, { ...payload, from: socket.clientId })));\n break;\n }\n }\n }\n\n if (parsed.type === WS_MESSAGE.DRONE_TELEMETRY) {\n const payload = parsed.payload || {};\n const json = JSON.stringify(payload);\n if (Buffer.byteLength(json, \"utf8\") > 48 * 1024) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"DRONE_TELEMETRY payload too large.\" })));\n return;\n }\n broadcastExcept(createEnvelope(WS_MESSAGE.DRONE_TELEMETRY, { ...payload, relayFrom: socket.clientId }), socket);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_START) {\n handleRhizohVoiceLiveStartV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_CHUNK) {\n handleRhizohVoiceLiveChunkV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_STOP) {\n handleRhizohVoiceLiveStopV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_SOCIAL_PULSE) {\n handleCastleSocialPulse(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_WAL_PEER_FEED) {\n handleCastleWalPeerFeed(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.OPEN_DATA_QUERY) {\n queryOpenData(parsed.payload)\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: true, result })));\n })\n .catch((e) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: false, error: e.message })));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_QUERY) {\n runRhizohBrainV2({\n ...(parsed.payload || {}),\n uid: socket?.auth?.user?.uid || \"unknown\"\n })\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, result)));\n })\n .catch(async () => {\n const fallback = await runRhizohBrain(parsed.payload || {}).catch(() => ({\n ok: false,\n reply: \"Rhizoh temporary disturbance. Habitat remains stable.\"\n }));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, fallback)));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.BROADCAST_REGISTER) {\n const role = String(parsed.payload?.role || \"\");\n if (role === \"GENESIS_BROADCAST_AGENT\") {\n broadcasterClientId = socket.clientId;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, registered: role })));\n broadcastState();\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.STUDIO_CUE) {\n // Only registered broadcaster can push global cues.\n if (!broadcasterClientId || socket.clientId !== broadcasterClientId) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Only broadcaster agent can emit STUDIO_CUE.\" })));\n return;\n }\n const cue = parsed.payload || {};\n if (typeof cue !== \"object\") {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid STUDIO_CUE payload.\" })));\n return;\n }\n broadcast(createEnvelope(WS_MESSAGE.STUDIO_CUE, cue));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_JOIN_ROOM) {\n const requested = String(parsed.payload?.roomId || \"\").trim();\n const requestedName = String(parsed.payload?.roomName || \"\").trim();\n const roomId = ensureRoom(requested || `room-${Math.random().toString(36).slice(2, 7)}`, requestedName || null);\n spiralState.activeByClient.set(socket.clientId, roomId);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, roomId })));\n broadcastSpiralState();\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_CREATE_CHARACTER) {\n const name = String(parsed.payload?.name || \"\").trim().slice(0, 32) || `AI-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;\n const role = String(parsed.payload?.role || \"agent\").trim().slice(0, 24) || \"agent\";\n const roomId = ensureRoom(\n String(parsed.payload?.roomId || spiralState.activeByClient.get(socket.clientId) || spiralState.rooms[0].id),\n null\n );\n\n // Character creation always maps to an authoritative world agent.\n const result = orchestrator.applyCommand({\n command: COMMAND.SPAWN_AGENT,\n params: { targetCastleId: \"castle-01\" }\n });\n if (!result?.ok || !result?.data?.id) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Character creation failed.\" })));\n return;\n }\n const id = result.data.id;\n spiralState.characters.set(id, { id, name, role, roomId, source: \"spawned\" });\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, characterId: id, roomId })));\n broadcastSpiralState();\n return;\n }\n });\n\n socket.on(\"close\", () => {\n if (socket.clientId === broadcasterClientId) broadcasterClientId = null;\n clientStats.delete(socket.clientId);\n rhizohVoiceLiveSessions.delete(socket);\n spiralState.activeByClient.delete(socket.clientId);\n removeCastleSocialClientFromAllRooms(socket.clientId);\n broadcast(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }));\n broadcastState();\n });\n});\n\nsetInterval(() => {\n void (async () => {\n try {\n const { frame, snapshot, delta, packedDelta } = orchestrator.tick(1 / 20);\n syncSpiralCharactersWithWorld(snapshot);\n broadcast(createEnvelope(WS_MESSAGE.NETWORK_TICK, { frame }));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_TICK, snapshot));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA, delta));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA_PACKED, packedDelta));\n broadcastSpiralState();\n await runAcademyEventTick();\n const executed = await tickAutomations(Date.now());\n for (const a of executed) {\n await addTranscript(a.uid, {\n source: \"planner\",\n eventType: \"automation-executed\",\n text: `Automation executed: ${a.title} (${a.action})`,\n roomId: \"studio-main\",\n meta: { automationId: a.id, action: a.action, deviceId: a.deviceId || \"\" }\n });\n }\n } catch (e) {\n console.error(\"[GATEWAY] world tick / automations loop error:\", String(e?.message || e));\n }\n })();\n}, 50);\n\n(async () => {\n await rhizohRuntime.telemetry.initOpenTelemetry();\n if (String(process.env.CASTLE_GENESIS_DISK_PERSIST ?? \"\").trim() === \"\") {\n process.env.CASTLE_GENESIS_DISK_PERSIST = \"1\";\n }\n const genesisDiskBoot = await hydrateGenesisContinuityPersistenceBootV0();\n if (!genesisDiskBoot.ok) {\n console.warn(\"[GATEWAY] genesis disk hydrate failed; continuing with in-memory genesis continuity\");\n }\n httpServer.listen(PORT, () => {\n startGenesisCanonicalClock();\n installGenesisCheckpointSurfaceGetter(buildGenesisRuntimeSurfacePayloadLive);\n setGenesisContinuityAfterPublishHook((seq) => {\n noteGenesisCheckpointSeqCommitted(seq);\n noteGenesisContinuitySeqAcceptedV0(seq);\n });\n startGenesisContinuityInfraSampler(buildGenesisRuntimeSurfacePayloadLive, 2000);\n logProductionObservatorySurfaceGuardsV0();\n logGatewaySubstrateAuthorityGuardsV0();\n console.log(`[GATEWAY] ws/http://localhost:${PORT}`);\n console.log(\n `[GATEWAY] genesis continuity: GET ${rhizohRuntime.routes.genesisRuntime} | POST ${rhizohRuntime.routes.genesisIngress} | SSE ${rhizohRuntime.routes.genesisStream} | replay ${rhizohRuntime.routes.genesisReplay} | diff ${rhizohRuntime.routes.genesisReplayDiff} | equiv ${rhizohRuntime.routes.genesisReplayEquivalence} | analytics ${rhizohRuntime.routes.genesisReplayAnalytics} | evolution ${rhizohRuntime.routes.genesisReplayEvolution}`\n );\n if (String(process.env.CASTLE_GENESIS_OBSERVATORY_LIVE_V01 ?? \"\").trim() === \"1\") {\n console.log(\"GENESIS OBSERVATORY LIVE v0.1 — READ ONLY PHASE STARTED\");\n }\n });\n})().catch((e) => {\n console.error(\"[GATEWAY] fatal boot (before or during listen):\", e);\n process.exit(1);\n});\n"}, "1d9d8741bd4e95f26c2ebe46625c67486740f83a": {"t": 1785754153.1413486, "n": 164314, "h": "cb0efa379ced3fc8", "c": "import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport dotenv from \"dotenv\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconsole.log(\"🔥 [GENESIS_BOOT] entry file:\", __filename);\nconsole.log(\"🔥 [GENESIS_BOOT] node env:\", process.env.NODE_ENV);\nconsole.log(\"🔥 [GENESIS_BOOT] pid:\", process.pid);\nconsole.log(\"🧬 [GENESIS_BOOT] cwd:\", process.cwd());\nconsole.log(\"🧬 [GENESIS_BOOT] import.meta.url:\", import.meta.url);\ndotenv.config({ path: path.join(__dirname, \"..\", \".env\") });\ndotenv.config({ path: path.join(__dirname, \"..\", \".env.local\"), override: true });\n\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport { createServer } from \"node:http\";\nimport { createOrchestrator } from \"@castle/orchestrator\";\nimport { parseTextToCommand } from \"@castle/command-dsl\";\nimport {\n WS_MESSAGE,\n COMMAND,\n createEnvelope,\n createRhizohImmutableEventV0,\n createRhizohPayloadRefV0,\n safeJsonParse,\n validateRhizohVoiceLiveChunkPayloadV0\n} from \"@castle/protocol\";\nimport { queryOpenData } from \"./openData.js\";\nimport { verifyClientToken } from \"./auth.js\";\nimport { runRhizohBrain } from \"./rhizohBrain.js\";\nimport { queryRhizohLlm } from \"./rhizohLlmGateway.js\";\nimport { rhizohGatewayTurn } from \"./rhizohGatewayTurn.js\";\nimport {\n RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n runRhizohVoiceTranscribeV3,\n rhizohVoiceTranscribeEnvV3\n} from \"./rhizohVoiceTranscribeV3.js\";\nimport { createHttpCorsPolicy, normalizeHttpOrigin, parseAllowedOriginsFromEnv } from \"./httpCorsPolicyV1.js\";\nimport {\n applyLanguagePropagationToLlmPayloadV1,\n applyLanguagePropagationToVoiceBodyV1,\n buildRhizohLanguagePropagationEchoV1,\n parseRhizohLanguagePropagationV1\n} from \"./rhizohLanguagePropagationV1.js\";\nimport { trimRhizohLlmGatewayPayloadV1 } from \"./rhizohLlmPayloadTrimV1.js\";\nimport {\n meshAppendDelta,\n meshContinuityAggregate,\n meshJoin,\n meshLeave,\n meshReplayFromNodeId,\n meshReplayFromSeq,\n meshSnapshot,\n meshSubscribeSse\n} from \"./presenceRoomMesh.js\";\nimport { runRhizohBrainV2 } from \"./rhizohBrainV2.js\";\nimport {\n listConnections,\n createConnection,\n updateConnection,\n deleteConnection,\n setDefaultConnection,\n resolveConnection\n} from \"./llmConnectionsStore.js\";\nimport { checkHttpRateLimit, getHttpClientIp } from \"./castleHttpRateLimit.js\";\nimport {\n submitAbuseReportV0,\n listModerationQueueV0,\n softBlockUserV0,\n removeSoftBlockV0,\n listSoftBlockedUsersV0\n} from \"./ops/moderationMvpV0.js\";\nimport { listRecentAgentSnapshotsV0 } from \"./ops/agentObservabilityV0.js\";\nimport { readAgentContainmentConfigV0 } from \"./ops/agentContainmentV0.js\";\nimport { readCostContainmentConfigV0, getCostContainmentStatsV0 } from \"./ops/costContainmentV0.js\";\nimport { getGlobalCostLedgerSnapshotV0, getGclBackendStatusV0 } from \"./ops/globalCostLedgerV0.js\";\nimport { evaluateEconomicStrategyL3V0 } from \"./ops/economicStrategyEngineL3V0.js\";\nimport { getPhasedRolloutStatsV0, readPhasedRolloutTierV0 } from \"./ops/phasedRolloutV0.js\";\nimport { buildUnifiedStateNarrativeV0 } from \"./ops/unifiedStateNarrativeV0.js\";\nimport { resolveHardeningTenantIdV0 } from \"./ops/narrativeTenantIsolationV0.js\";\nimport {\n classifyStressResponseV0,\n verifyStressTaxonomyCoverageV0,\n verifyStressConflictResolutionV0\n} from \"./ops/stressResponseTaxonomyV0.js\";\nimport { verifyResolutionStabilityEnvelopeV0 } from \"./ops/resolutionStabilityEnvelopeV0.js\";\nimport { logLlmAccess } from \"./castleLlmAudit.js\";\nimport { appendMemory, listMemories, getMemoryContext, getPersonaGoalMemory, setPersonaGoalMemory, autoCompactMemories } from \"./memoryStore.js\";\nimport { getFirebasePersistence } from \"./firebasePersistence.js\";\nimport { registerAgentIdentity, listAgentIdentities, getAgentIdentity, updateAgentIdentity } from \"./agentIdentityStore.js\";\nimport { queueAcademyEvent, listAcademyEvents, resolveAcademyEvent, runAcademyEventTick } from \"./academyEventEngine.js\";\nimport { enqueuePdfJob, getPdfJob } from \"./pdfPipeline.js\";\nimport {\n createPublishSession,\n listPublishSessions,\n updatePublishSession,\n addTranscript,\n listTranscripts,\n searchMetadata,\n registerDevice,\n listDevices,\n updateDevice,\n appendTelemetry,\n listTelemetry,\n createAutomation,\n listAutomations,\n tickAutomations,\n enqueueCommand,\n listCommands,\n updateCommandStatus,\n registerSocialChannel,\n listSocialChannels\n} from \"./studioOpsStore.js\";\nimport { metrics as infraMetrics } from \"./infra/metrics.js\";\nimport { renderRhizohPrometheusMetrics, rhizohEnterpriseMetrics } from \"./infra/rhizohEnterpriseMetrics.js\";\nimport { scoreHealth } from \"./infra/healthScore.js\";\nimport { getTraceById, listTracesBySession } from \"./infra/traceRegistry.js\";\nimport { canonicalEpistemicSealString, hashAndSignEpistemicSeal, EPISTEMIC_SEAL_SCHEMA } from \"./epistemicSeal.js\";\nimport { persistEpistemicLedgerBatch } from \"./epistemicLedgerStore.js\";\nimport { persistEpistemicForecastBatch } from \"./epistemicForecastStore.js\";\nimport { buildRhizohExternalGroundTruthPayload } from \"./rhizohExternalGroundTruthGateway.js\";\nimport { ingestRhizohExternalLossBatchHttp } from \"./rhizohExternalLossIngestGateway.js\";\nimport {\n ingestRhizohProductOutcomeHttp,\n listRhizohProductOutcomeAggregates,\n verifyRhizohOutcomeHmac,\n verifyRhizohOutcomeSourceToken\n} from \"./rhizohProductOutcomeIngestGateway.js\";\nimport { initRhizoh } from \"./rhizohProductionBootstrap.js\";\nimport { handleAcademicObservatoryExportGetV0 } from \"./rhizoh/academicObservatoryHttpV0.js\";\nimport {\n handleLiveSportsBundleGetV0,\n handleLiveNewsHeadlinesGetV0,\n handleLiveWorldFeedGetV0\n} from \"./rhizoh/liveWorldFeedHttpV0.js\";\nimport {\n buildGenesisRuntimeSurfacePayload,\n recordGenesisEpistemicLedgerPersisted,\n recordGenesisEpistemicSealIssued,\n startGenesisCanonicalClock\n} from \"./genesisRuntimeSurfaceV0.js\";\nimport { registerGenesisSseClient, getGenesisContinuitySeq, setGenesisContinuityAfterPublishHook } from \"./genesisContinuityStreamHubV0.js\";\nimport { computeGenesisReplayFingerprintV0 } from \"./genesisReplayFingerprintV0.js\";\nimport { buildGenesisGatewayCapabilitiesV0 } from \"./genesisGatewayCapabilitiesV0.js\";\nimport {\n installGenesisCheckpointSurfaceGetter,\n noteGenesisCheckpointSeqCommitted,\n getLatestGenesisSignedCheckpoint\n} from \"./genesisContinuityCheckpointV0.js\";\nimport { hydrateGenesisContinuityPersistenceBootV0 } from \"./genesisContinuityHydrateBootV0.js\";\nimport { startGenesisContinuityInfraSampler } from \"./genesisContinuityInfraSamplerV0.js\";\nimport {\n genesisCheckpointQueryBySeqV0,\n genesisCheckpointQueryRangeV0,\n genesisCheckpointQueryLineageV0\n} from \"./genesisCheckpointQueryV0.js\";\nimport {\n genesisContinuityEventArchiveEnabled,\n queryGenesisContinuityEventArchiveV0,\n GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION\n} from \"./genesisContinuityEventArchiveV0.js\";\nimport { resolveGenesisReplayRouterV1, GENESIS_REPLAY_ROUTER_SCHEMA } from \"./genesisReplayRouterV1.js\";\nimport {\n computeGenesisReplayTemporalDiffV1,\n GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA\n} from \"./genesisReplayTemporalDiffV1.js\";\nimport { compareGenesisReplayEquivalenceV1 } from \"./genesisReplayEquivalenceV1.js\";\nimport { computeGenesisReplayAnalyticsV1 } from \"./genesisReplayAnalyticsV1.js\";\nimport { computeGenesisReplayEvolutionV1 } from \"./genesisReplayEvolutionV1.js\";\nimport { handleStripeCheckoutCreate, handleStripeMembershipWebhook } from \"./stripeWebhookMembership.js\";\nimport { buildRealityHealthPayload } from \"./edgeRealityConsistencyV1.js\";\nimport {\n assertWalPeerSocketAuthorityV0,\n buildGatewaySubstrateAuthoritySnapshotV0,\n logGatewaySubstrateAuthorityGuardsV0,\n validateWalPeerFeedSignatureV0\n} from \"./gatewaySubstrateAuthorityV0.js\";\nimport {\n buildSubstrateOperationalSnapshotV0,\n recordWalPeerRejectV0,\n renderSubstratePrometheusMetricsV0\n} from \"./infra/substrateOperationalMetrics.js\";\nimport { ingestClientSubstrateHealthV0 } from \"./rhizohSubstrateHealthIngestV0.js\";\nimport { recordSubstrateOtelEventV0 } from \"./infra/opentelemetrySubstrateV0.js\";\nimport {\n GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n getGenesisWorldObservationIngressStatusV0,\n ingestGenesisWorldObservationV0\n} from \"./genesisContinuityClientIngressV0.js\";\nimport {\n getGenesisContinuitySeqAuditV0,\n noteGenesisContinuitySeqAcceptedV0\n} from \"./genesisContinuitySeqGapV0.js\";\n\n/** Render / Fly / Railway: `PORT` — yoksa `CASTLE_GATEWAY_PORT` — yoksa 8090. */\nconst PORT =\n Number(process.env.PORT) ||\n Number(process.env.CASTLE_GATEWAY_PORT) ||\n 8090;\n/** Spiral / world WebSocket server — set after `wss` init; used by genesis runtime surface. */\nlet spiralWssForGenesis = null;\nconst MAX_MESSAGE_BYTES = Number(process.env.CASTLE_MAX_MESSAGE_BYTES || 32 * 1024);\nconst REQUIRED_GATEWAY_TOKEN = process.env.CASTLE_GATEWAY_TOKEN || \"\";\nconst ALLOWED_ORIGINS = parseAllowedOriginsFromEnv().fromList;\nconst httpCors = createHttpCorsPolicy();\n\n/** @returns {string|null} `Access-Control-Allow-Origin`; null = browser CORS fail. */\nfunction accessControlAllowOriginValue(req) {\n return httpCors.accessControlAllowOriginValue(req);\n}\n\nfunction applyHttpCorsHeaders(req, res) {\n return httpCors.applyHttpCorsHeaders(req, res);\n}\nconst REQUIRE_AUTH = process.env.CASTLE_REQUIRE_AUTH === \"true\";\nconst ALLOW_DEV_ANON = process.env.CASTLE_ALLOW_DEV_ANON !== \"false\";\nconst ALLOW_DEV_HTTP_UID = process.env.CASTLE_ALLOW_DEV_HTTP_UID !== \"false\";\n/** Presence mesh: allow `X-Castle-Guest-Id` when Firebase auth absent (default on). Set `false` to require auth only. */\nconst ALLOW_MESH_GUEST = process.env.CASTLE_ALLOW_MESH_GUEST !== \"false\";\nconst SOCIAL_RETRY_MAX = Math.max(0, Math.min(5, Number(process.env.CASTLE_SOCIAL_RETRY_MAX || 2)));\nconst SOCIAL_RETRY_BASE_MS = Math.max(100, Number(process.env.CASTLE_SOCIAL_RETRY_BASE_MS || 700));\nconst TELEGRAM_BOT_TOKEN = process.env.CASTLE_TELEGRAM_BOT_TOKEN || \"\";\nconst WHATSAPP_TOKEN = process.env.CASTLE_WHATSAPP_TOKEN || \"\";\nconst WHATSAPP_PHONE_NUMBER_ID = process.env.CASTLE_WHATSAPP_PHONE_NUMBER_ID || \"\";\nconst RL_RHIZOH_LLM_PER_MIN = Math.max(5, Number(process.env.CASTLE_RL_RHIZOH_LLM_PER_MIN || 40));\nconst RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN || 30)\n);\nconst RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN || 120));\nconst RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN = Math.max(\n 10,\n Number(process.env.CASTLE_RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN || 60)\n);\nconst RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN = Math.max(8, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN || 48));\nconst RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN = Math.max(4, Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN || 24));\nconst RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN || 18)\n);\nconst RL_EPISTEMIC_SEAL_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_EPISTEMIC_SEAL_PER_MIN || 120));\nconst RL_EPISTEMIC_LOG_PER_MIN = Math.max(20, Number(process.env.CASTLE_RL_EPISTEMIC_LOG_PER_MIN || 240));\nconst EPISTEMIC_SEAL_SECRET = String(\n process.env.CASTLE_EPISTEMIC_SEAL_SECRET || process.env.CASTLE_GATEWAY_TOKEN || \"\"\n).trim();\nconst RL_LLM_CONN_TEST_PER_MIN = Math.max(2, Number(process.env.CASTLE_RL_LLM_CONNECTION_TEST_PER_MIN || 8));\n\n/** One-shot production warnings: CORS surface, auth toggle, dev bypass flags (observatory deploy hygiene). */\nfunction logProductionObservatorySurfaceGuardsV0() {\n if (String(process.env.NODE_ENV || \"\").toLowerCase() !== \"production\") return;\n const tag = \"[GATEWAY_PROD_GUARD]\";\n const primaryCors = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n const corsListEmpty = ALLOWED_ORIGINS.length === 0;\n if (corsListEmpty && !primaryCors) {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty and CASTLE_HTTP_CORS_ORIGIN empty — production still merges RHIZOH_BROWSER_ORIGINS_V1 defaults; set env for preview/staging hosts`\n );\n } else if (corsListEmpty && primaryCors && primaryCors !== \"*\") {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty — only CASTLE_HTTP_CORS_ORIGIN set; add web.app + firebaseapp.com (and previews) to CASTLE_ALLOWED_ORIGINS if clients use alternate hostnames`\n );\n }\n if (primaryCors && /localhost|127\\.0\\.0\\.1/i.test(primaryCors)) {\n console.warn(`${tag} CORS: CASTLE_HTTP_CORS_ORIGIN is localhost while NODE_ENV=production`);\n }\n if (REQUIRE_AUTH) {\n console.warn(\n `${tag} AUTH: CASTLE_REQUIRE_AUTH=true — clients must send a valid gateway credential (otherwise silent 401 on replay/SSE); EventSource cannot set custom headers unless proxied`\n );\n if (REQUIRED_GATEWAY_TOKEN.length < 16) {\n console.warn(`${tag} AUTH: CASTLE_GATEWAY_TOKEN missing or short while require-auth enabled`);\n }\n }\n if (ALLOW_DEV_ANON) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_ANON is not \"false\" — dev anonymous path may be live in production (set CASTLE_ALLOW_DEV_ANON=false)`\n );\n }\n if (ALLOW_DEV_HTTP_UID) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_HTTP_UID is not \"false\" — X-Castle-Dev-Uid bypass risk (set CASTLE_ALLOW_DEV_HTTP_UID=false)`\n );\n }\n}\n\nconst rhizohRuntime = initRhizoh();\n\n/** İstemci: llmKeySource veya keySource — env | user_connection | auto */\nfunction normalizeClientLlmKeySource(raw) {\n const s = String(raw ?? \"\").trim().toLowerCase();\n if (s === \"env\" || s === \"server\") return \"env\";\n if (s === \"user_connection\" || s === \"connection\" || s === \"user\") return \"user_connection\";\n if (s === \"auto\" || s === \"\") return \"auto\";\n return null;\n}\n\nfunction sendJson(res, statusCode, payload) {\n res.writeHead(statusCode, { \"Content-Type\": \"application/json; charset=utf-8\" });\n res.end(JSON.stringify(payload));\n}\n\nfunction logRhizohHealth(stage, detail = {}) {\n try {\n console.info(`[CASTLE_${String(stage || \"unknown\")}]`, detail && typeof detail === \"object\" ? detail : {});\n } catch {\n /* noop */\n }\n}\n\nfunction renderInfraMetricsProm() {\n const lines = [\n `castle_gateway_events_processed_total ${infraMetrics.eventsProcessed}`,\n `castle_gateway_duplicate_reject_total ${infraMetrics.duplicateRejects}`,\n `castle_gateway_enqueue_latency_ms ${infraMetrics.enqueueLatencyMs}`,\n `castle_gateway_queue_depth ${infraMetrics.queueDepth}`,\n `castle_gateway_queue_lag_seconds ${(Number(infraMetrics.queueLag || 0) / 1000).toFixed(3)}`,\n `castle_gateway_errors_total ${infraMetrics.errors}`\n ];\n const buckets = infraMetrics.enqueueLatencyBuckets || {};\n for (const k of Object.keys(buckets)) {\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"${k}\"} ${buckets[k]}`);\n }\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"+Inf\"} ${infraMetrics.eventsProcessed}`);\n if (process.env.CASTLE_SUBSTRATE_METRICS !== \"0\") {\n lines.push(renderSubstratePrometheusMetricsV0().trimEnd());\n }\n return lines.join(\"\\n\");\n}\n\nasync function fetchJsonWithTimeout(url, timeoutMs = 1200) {\n const ctl = new AbortController();\n const tid = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const res = await fetch(url, { signal: ctl.signal });\n if (!res.ok) return null;\n return await res.json();\n } catch {\n return null;\n } finally {\n clearTimeout(tid);\n }\n}\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction getHttpPathname(req) {\n let p = String(req.url || \"\").split(\"?\")[0];\n /* Reverse proxies / clients sometimes append `/` — avoid silent 404 on e.g. `/rhizoh/genesis/stream/`. */\n while (p.length > 1 && p.endsWith(\"/\")) {\n p = p.slice(0, -1);\n }\n return p;\n}\n\nfunction rhizohLlmEnvConfigured() {\n const p = String(process.env.CASTLE_LLM_PROVIDER || \"openai\").toLowerCase();\n const key =\n p === \"anthropic\"\n ? process.env.ANTHROPIC_API_KEY\n : p === \"gemini\"\n ? process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY\n : p === \"xai\"\n ? process.env.XAI_API_KEY\n : p === \"deepseek\"\n ? process.env.DEEPSEEK_API_KEY\n : p === \"mistral\"\n ? process.env.MISTRAL_API_KEY\n : p === \"openrouter\"\n ? process.env.OPENROUTER_API_KEY\n : process.env.OPENAI_API_KEY;\n return !!String(key || \"\").trim();\n}\n\n/**\n * @param {import(\"firebase-admin/firestore\").Firestore | null} db\n * @param {number} timeoutMs\n */\nasync function probeFirestoreReachable(db, timeoutMs = 2000) {\n if (!db) return { ok: false, reason: \"no_db\" };\n const timeout = sleep(timeoutMs).then(() => ({ ok: false, reason: \"timeout\" }));\n const work = db\n .collection(\"_castle_gateway_health\")\n .doc(\"ping\")\n .get()\n .then(() => ({ ok: true, reason: \"reachable\" }))\n .catch((e) => ({ ok: false, reason: String(e?.message || e || \"firestore_error\") }));\n return Promise.race([work, timeout]);\n}\n\nfunction stripHtml(text = \"\") {\n return String(text)\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \" \")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \" \")\n .replace(/<[^>]+>/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction haversineMeters(aLat, aLon, bLat, bLon) {\n const R = 6371000;\n const dLat = ((bLat - aLat) * Math.PI) / 180;\n const dLon = ((bLon - aLon) * Math.PI) / 180;\n const sa = Math.sin(dLat / 2);\n const sb = Math.sin(dLon / 2);\n const x = sa * sa + Math.cos((aLat * Math.PI) / 180) * Math.cos((bLat * Math.PI) / 180) * sb * sb;\n return 2 * R * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));\n}\n\nasync function checkPublishTargetHealth(protocol, target) {\n const p = String(protocol || \"\").toUpperCase();\n const t = String(target || \"\").trim();\n if (!t) return { ok: false, reason: \"target_missing\", shouldReconnect: false };\n if (p === \"RTMP\") {\n const ok = t.startsWith(\"rtmp://\") || t.startsWith(\"rtmps://\");\n return { ok, reason: ok ? \"rtmp_target_format_ok\" : \"invalid_rtmp_target\", shouldReconnect: !ok, reconnectInSec: 15 };\n }\n if (p === \"WHIP\" || p === \"SFU\") {\n try {\n const res = await fetch(t, { method: \"OPTIONS\" });\n const ok = res.status < 500;\n return { ok, reason: `http_${res.status}`, shouldReconnect: !ok, reconnectInSec: 8 };\n } catch {\n return { ok: false, reason: \"network_unreachable\", shouldReconnect: true, reconnectInSec: 8 };\n }\n }\n return { ok: true, reason: \"unchecked_protocol\", shouldReconnect: false };\n}\n\nasync function fetchTextFromUrl(url) {\n const res = await fetch(url, { method: \"GET\" });\n if (!res.ok) throw new Error(`fetch_${res.status}`);\n const ct = String(res.headers.get(\"content-type\") || \"\");\n if (ct.includes(\"application/pdf\")) throw new Error(\"pdf_binary_not_supported_directly\");\n const body = await res.text();\n return stripHtml(body).slice(0, 14000);\n}\n\nfunction buildTaskActionGraph(task = \"\", context = {}) {\n const t = String(task || \"\").toLowerCase();\n if (t.includes(\"dinner\") || t.includes(\"akşam yemeği\") || t.includes(\"aksam yemegi\")) {\n return {\n taskTitle: \"Dinner Prep Planner\",\n nodes: [\n { id: \"n1\", title: \"Kitchen preheat\", action: \"smart-oven.preheat\", params: { temperatureC: 190, mode: \"fan\" } },\n { id: \"n2\", title: \"Lighting comfort\", action: \"smart-light.set_scene\", params: { scene: \"warm_dinner\" } },\n { id: \"n3\", title: \"Fridge inventory check\", action: \"smart-fridge.inventory_scan\", params: {} },\n { id: \"n4\", title: \"Reminder notification\", action: \"smart-watch.notify\", params: { text: \"Dinner prep started.\" } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" },\n { from: \"n3\", to: \"n4\" }\n ]\n };\n }\n return {\n taskTitle: task || \"General Planner\",\n nodes: [\n { id: \"n1\", title: \"Safety check\", action: \"device.safety_check\", params: { context } },\n { id: \"n2\", title: \"Primary action\", action: \"device.execute_task\", params: { task } },\n { id: \"n3\", title: \"Report\", action: \"studio.timeline_note\", params: { task } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" }\n ]\n };\n}\n\nfunction buildEthicsPrograms() {\n return {\n policyVersion: \"v1\",\n tracks: [\n {\n id: \"elderly-care-assist\",\n audience: \"elderly\",\n title: \"Elderly Assistance Track\",\n principles: [\"human-in-the-loop\", \"consent-first\", \"low-friction alerts\"],\n modules: [\"medication-reminder\", \"fall-risk-check\", \"family-notify\", \"daily-companion-brief\"]\n },\n {\n id: \"kids-safe-learning\",\n audience: \"children\",\n title: \"Kids Safe Learning Track\",\n principles: [\"guardian-consent\", \"age-appropriate-language\", \"content-filtering\"],\n modules: [\"study-coach\", \"screen-time-balance\", \"safe-social-share\", \"emergency-contact\"]\n }\n ]\n };\n}\n\nasync function deliverViaTelegram(channel, text) {\n const chatId = String(channel.endpoint || \"\").trim();\n if (!TELEGRAM_BOT_TOKEN) throw new Error(\"telegram_token_missing\");\n if (!chatId) throw new Error(\"telegram_chat_id_missing\");\n const url = `https://api.telegram.org/bot${encodeURIComponent(TELEGRAM_BOT_TOKEN)}/sendMessage`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ chat_id: chatId, text })\n });\n if (!res.ok) throw new Error(`telegram_http_${res.status}`);\n return { provider: \"telegram\", status: res.status };\n}\n\nasync function deliverViaWhatsapp(channel, text) {\n const to = String(channel.endpoint || \"\").trim();\n if (!WHATSAPP_TOKEN) throw new Error(\"whatsapp_token_missing\");\n if (!WHATSAPP_PHONE_NUMBER_ID) throw new Error(\"whatsapp_phone_number_id_missing\");\n if (!to) throw new Error(\"whatsapp_to_missing\");\n const url = `https://graph.facebook.com/v22.0/${encodeURIComponent(WHATSAPP_PHONE_NUMBER_ID)}/messages`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${WHATSAPP_TOKEN}` },\n body: JSON.stringify({ messaging_product: \"whatsapp\", to, type: \"text\", text: { body: text } })\n });\n if (!res.ok) throw new Error(`whatsapp_http_${res.status}`);\n return { provider: \"whatsapp\", status: res.status };\n}\n\nasync function deliverViaWebhook(channel, text) {\n const endpoint = String(channel.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"webhook_endpoint_missing\");\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text, platform: channel.platform || \"webhook\", ts: Date.now() })\n });\n if (!res.ok) throw new Error(`webhook_http_${res.status}`);\n return { provider: \"webhook\", status: res.status };\n}\n\nasync function deliverSocialMessage(channel, text) {\n const platform = String(channel.platform || \"webhook\").toLowerCase();\n if (platform === \"telegram\") return deliverViaTelegram(channel, text);\n if (platform === \"whatsapp\") return deliverViaWhatsapp(channel, text);\n return deliverViaWebhook(channel, text);\n}\n\nasync function deliverSocialWithRetry(channel, text) {\n let attempt = 0;\n let lastError = null;\n while (attempt <= SOCIAL_RETRY_MAX) {\n try {\n const result = await deliverSocialMessage(channel, text);\n return { ok: true, attempt: attempt + 1, ...result };\n } catch (error) {\n lastError = error;\n if (attempt >= SOCIAL_RETRY_MAX) break;\n const jitter = Math.floor(Math.random() * 90);\n await sleep(SOCIAL_RETRY_BASE_MS * Math.pow(2, attempt) + jitter);\n }\n attempt += 1;\n }\n return { ok: false, attempt: attempt + 1, error: String(lastError?.message || \"delivery_failed\") };\n}\n\nasync function dispatchCommandToAdapter(device, command) {\n const adapter = String(command?.adapter || device?.adapter || \"websocket\").toLowerCase();\n const endpoint = String(device?.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"device_endpoint_missing\");\n if (adapter === \"mqtt\") {\n // MQTT delivery is proxied via a bridge endpoint (HTTP webhook style).\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n type: \"mqtt-command\",\n topic: `castle/devices/${device.id}/commands`,\n action: command.action,\n params: command.params || {}\n })\n });\n if (!res.ok) throw new Error(`mqtt_bridge_http_${res.status}`);\n return { ok: true, adapter, via: \"http-bridge\", status: res.status };\n }\n if (!endpoint.startsWith(\"ws://\") && !endpoint.startsWith(\"wss://\")) throw new Error(\"invalid_websocket_endpoint\");\n const sendResult = await new Promise((resolve, reject) => {\n const ws = new WebSocket(endpoint);\n const timer = setTimeout(() => {\n try {\n ws.close();\n } catch {\n /* noop */\n }\n reject(new Error(\"websocket_timeout\"));\n }, 3000);\n ws.on(\"open\", () => {\n ws.send(\n JSON.stringify({\n type: \"castle-command\",\n commandId: command.id,\n deviceId: command.deviceId,\n action: command.action,\n params: command.params || {}\n }),\n (err) => {\n clearTimeout(timer);\n ws.close();\n if (err) reject(new Error(\"websocket_send_failed\"));\n else resolve({ ok: true, adapter: \"websocket\", via: \"ws\" });\n }\n );\n });\n ws.on(\"error\", () => {\n clearTimeout(timer);\n reject(new Error(\"websocket_connect_failed\"));\n });\n });\n return sendResult;\n}\n\nfunction readHttpJson(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => {\n if (!buf) return resolve({});\n const payload = safeJsonParse(buf);\n if (!payload || typeof payload !== \"object\") return reject(new Error(\"invalid_json_payload\"));\n resolve(payload);\n });\n req.on(\"error\", reject);\n });\n}\n\n/** Ham gövde (HMAC doğrulaması için). */\nfunction readHttpBodyText(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => resolve(buf));\n req.on(\"error\", reject);\n });\n}\n\nfunction maskOpaqueId(value) {\n const t = String(value || \"\");\n if (!t) return \"(empty)\";\n if (t.length <= 8) return \"****\";\n return `${t.slice(0, 4)}…${t.slice(-2)}`;\n}\n\nfunction readGatewayHttpToken(req) {\n const x = String(req.headers?.[\"x-castle-gateway-token\"] || \"\").trim();\n if (x) return x;\n const auth = String(req.headers?.authorization || \"\");\n const m = /^Bearer\\s+(.+)$/i.exec(auth);\n return m ? String(m[1] || \"\").trim() : \"\";\n}\n\nasync function resolveHttpUser(req) {\n const authResult = await verifyClientToken(req);\n if (authResult?.ok && authResult.user?.uid) {\n return { ok: true, uid: String(authResult.user.uid), mode: authResult.kind || \"auth\" };\n }\n if (ALLOW_DEV_ANON && ALLOW_DEV_HTTP_UID) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) return { ok: true, uid: `dev-${devUid.slice(0, 64)}`, mode: \"dev-header\" };\n }\n return { ok: false, reason: authResult?.reason || \"auth_required\" };\n}\n\n/**\n * Epistemic ledger ingest — Firebase uid OR valid transport token + X-Castle-Dev-Uid.\n * Prod disables CASTLE_ALLOW_DEV_HTTP_UID; LLM may still work while telemetry 401'd without this.\n */\nasync function resolveEpistemicIngestActor(req) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok && tok === REQUIRED_GATEWAY_TOKEN) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) {\n return { ok: true, uid: `gw-telemetry-${devUid.slice(0, 64)}`, mode: \"gateway-token-dev-uid\" };\n }\n return { ok: false, reason: \"dev_uid_required\" };\n }\n if (tok) {\n return { ok: false, reason: \"gateway_token_mismatch\" };\n }\n }\n const auth = await resolveHttpUser(req);\n if (auth.ok) return auth;\n return { ok: false, reason: auth.reason || \"auth_required\" };\n}\n\n/** Mesh actor: Firebase uid, dev header, or `guest:<uuid>` via header / `guestId` query (SSE cannot set headers). */\nasync function resolveMeshActor(req) {\n const auth = await resolveHttpUser(req);\n if (auth.ok) return { ok: true, clientUid: auth.uid, mode: auth.mode };\n if (ALLOW_MESH_GUEST) {\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const gid = String(req.headers?.[\"x-castle-guest-id\"] || q.searchParams.get(\"guestId\") || \"\").trim();\n if (gid) {\n const normalized = gid.startsWith(\"guest:\") ? gid.slice(0, 180) : `guest:${gid.slice(0, 160)}`;\n return { ok: true, clientUid: normalized, mode: \"guest-header\" };\n }\n }\n return { ok: false, reason: \"mesh_auth_required\" };\n}\n\n/** Live genesis runtime JSON (GET /rhizoh/genesis/runtime + SSE infra sampler). */\nasync function buildGenesisRuntimeSurfacePayloadLive() {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n let spiralWsActive = 0;\n if (spiralWssForGenesis) {\n for (const c of spiralWssForGenesis.clients) {\n if (c.readyState === 1) spiralWsActive += 1;\n }\n }\n const base = buildGenesisRuntimeSurfacePayload({\n infraMetrics,\n scoredHealth: scored,\n rhizohEnterpriseMetrics,\n mesh: meshContinuityAggregate(),\n workerHealth: worker,\n port: PORT,\n spiralWebSocketClientsActive: spiralWsActive\n });\n return {\n ...base,\n replayFingerprint: computeGenesisReplayFingerprintV0(base),\n gatewayCapabilities: buildGenesisGatewayCapabilitiesV0(),\n genesisStream: {\n schema: \"castle.genesis.stream_cursor.v0\",\n lastAcceptedSeq: getGenesisContinuitySeq(),\n clientIngress: {\n schema: GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n contract: \"castle.world_observation.ingress_envelope.v1\",\n postPath: rhizohRuntime.routes.genesisIngress,\n closure: getGenesisWorldObservationIngressStatusV0()\n },\n seqContinuity: getGenesisContinuitySeqAuditV0(),\n eventArchive: genesisContinuityEventArchiveEnabled()\n ? {\n enabled: true,\n projection: GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION,\n queryPath: rhizohRuntime.routes.genesisContinuityEvents\n }\n : { enabled: false },\n replayRouter: {\n schema: GENESIS_REPLAY_ROUTER_SCHEMA,\n queryPath: rhizohRuntime.routes.genesisReplay,\n queryHint: \"from=&to= or range=from-to; optional type=&checkpoints=0\",\n temporalDiffPath: rhizohRuntime.routes.genesisReplayDiff,\n temporalDiffSchema: GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA,\n equivalencePath: rhizohRuntime.routes.genesisReplayEquivalence,\n analyticsPath: rhizohRuntime.routes.genesisReplayAnalytics,\n evolutionPath: rhizohRuntime.routes.genesisReplayEvolution\n }\n }\n };\n}\n\nconst httpServer = createServer(async (req, res) => {\n const corsAllow = applyHttpCorsHeaders(req, res);\n if (req.method === \"OPTIONS\") {\n if (!corsAllow && req.headers?.origin) {\n console.warn(\"[GATEWAY_CORS] preflight rejected for origin:\", req.headers.origin);\n }\n res.writeHead(corsAllow ? 204 : 403);\n res.end();\n return;\n }\n\n const pathname = getHttpPathname(req);\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/genesis/__ping\") {\n sendJson(res, 200, {\n ok: true,\n pid: process.pid,\n entry: __filename,\n time: Date.now(),\n genesis: true\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/epistemic/__probe\") {\n sendJson(res, 200, {\n ok: true,\n epistemicIngestActorV2: true,\n routes: {\n seal: rhizohRuntime.routes.epistemicSeal,\n logsBatch: rhizohRuntime.routes.epistemicLogsBatch\n },\n hasGatewayToken: Boolean(REQUIRED_GATEWAY_TOKEN),\n gatewayTokenLen: REQUIRED_GATEWAY_TOKEN ? REQUIRED_GATEWAY_TOKEN.length : 0\n });\n return;\n }\n\n if (\n req.method === \"GET\" &&\n (pathname === \"/rhizoh/live/world-feed\" ||\n pathname === \"/rhizoh/live/sports-bundle\" ||\n pathname === \"/rhizoh/live/news-headlines\")\n ) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_live_world_feed:${ip}`, RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const out =\n pathname === \"/rhizoh/live/sports-bundle\"\n ? await handleLiveSportsBundleGetV0()\n : pathname === \"/rhizoh/live/news-headlines\"\n ? await handleLiveNewsHeadlinesGetV0(req)\n : await handleLiveWorldFeedGetV0(req);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/webhooks/stripe\") {\n await handleStripeMembershipWebhook(req, res);\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/billing/stripe/checkout\") {\n await handleStripeCheckoutCreate(req, res, { readHttpJson, resolveHttpUser, sendJson });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/live\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, live: false, dns: true, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n sendJson(res, 200, { ok: true, live: true, dns: true, service: \"castle-gateway\", ts: Date.now() });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/cors\") {\n const reqOrigin = normalizeHttpOrigin(req.headers?.origin);\n const allowOrigin = accessControlAllowOriginValue(req);\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n requestOrigin: reqOrigin || null,\n allowed: Boolean(allowOrigin),\n allowOrigin: allowOrigin || null,\n allowSetSize: httpCors.allowSet.size\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/ready\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, ready: false, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n try {\n const persistence = getFirebasePersistence();\n const llmProbe = rhizohLlmEnvConfigured();\n sendJson(res, 200, {\n ok: true,\n ready: true,\n dns: true,\n persistence: persistence.mode,\n service: \"castle-gateway\",\n gateway: \"ok\",\n llm: llmProbe ? \"ok\" : \"degraded\",\n spine: llmProbe ? \"ok\" : \"partial\"\n });\n } catch (e) {\n sendJson(res, 503, { ok: false, ready: false, reason: String(e?.message || e), service: \"castle-gateway\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/reality\") {\n const payload = buildRealityHealthPayload();\n sendJson(res, payload.ok ? 200 : 503, payload);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/substrate\") {\n const authority = buildGatewaySubstrateAuthoritySnapshotV0();\n const operational = buildSubstrateOperationalSnapshotV0();\n const drift = buildRealityHealthPayload();\n const ok = Boolean(authority.productionAuthoritySatisfied) && drift.ok;\n sendJson(res, ok ? 200 : 503, {\n ok,\n service: \"castle-gateway\",\n ts: Date.now(),\n authority,\n operational,\n drift: { ok: drift.ok, errors: drift.drift?.errors, warnings: drift.drift?.warnings }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/substrate/health\") {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const body = await readHttpJson(req);\n const uid = (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestClientSubstrateHealthV0(body, { uid, clientId: uid });\n if (!result.ok) {\n sendJson(res, 400, result);\n return;\n }\n recordSubstrateOtelEventV0(\"client_health_ingest\", { uid: String(uid).slice(0, 32) });\n sendJson(res, 200, result);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/deps\") {\n const t0 = Date.now();\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, {\n ok: false,\n dns: true,\n llm: false,\n firestore: false,\n phase: \"maintenance\",\n latencyMs: Date.now() - t0,\n service: \"castle-gateway\"\n });\n return;\n }\n const persistence = getFirebasePersistence();\n let firestoreOk = persistence.mode !== \"firebase\";\n let firestoreDetail = persistence.mode === \"file\" ? \"file_mode\" : \"not_applicable\";\n if (persistence.mode === \"firebase\" && persistence.db) {\n const pr = await probeFirestoreReachable(persistence.db, 2000);\n firestoreOk = pr.ok;\n firestoreDetail = pr.reason || (pr.ok ? \"reachable\" : \"fail\");\n } else if (persistence.mode === \"firebase\" && !persistence.db) {\n firestoreOk = false;\n firestoreDetail = \"firebase_not_initialized\";\n }\n const llmOk = rhizohLlmEnvConfigured();\n const overallOk = llmOk && firestoreOk;\n const latencyMs = Date.now() - t0;\n sendJson(res, 200, {\n ok: overallOk,\n dns: true,\n llm: llmOk,\n firestore: persistence.mode === \"firebase\" ? firestoreOk : false,\n persistence: persistence.mode,\n firestoreDetail,\n latencyMs,\n service: \"castle-gateway\",\n wsPort: PORT\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health\") {\n const persistence = getFirebasePersistence();\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n wsPort: PORT,\n persistence: persistence.mode,\n presenceMesh: \"sse\"\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/health\") {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n sendJson(res, 200, {\n ok: true,\n role: \"gateway\",\n queueLagMs: infraMetrics.queueLag,\n queueDepth: infraMetrics.queueDepth,\n errors: infraMetrics.errors,\n status: scored.status,\n score: scored.score,\n reasons: scored.reasons,\n worker: worker\n ? {\n status: worker.status,\n score: worker.score,\n reasons: worker.reasons\n }\n : null,\n timestamp: Date.now()\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisRuntime) {\n const payload = await buildGenesisRuntimeSurfacePayloadLive();\n sendJson(res, 200, payload);\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.genesisIngress) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const actor = await resolveMeshActor(req);\n const clientId = actor.ok ? actor.clientUid : (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestGenesisWorldObservationV0(body, { clientId });\n if (!result.ok) {\n const status =\n result.error === \"rate_limited\" || result.error === \"agent_spoke_throttled\"\n ? 429\n : result.error === \"payload_too_large\"\n ? 413\n : 400;\n sendJson(res, status, result);\n return;\n }\n sendJson(res, result.idempotent ? 200 : 202, result);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisStream) {\n const corsOrigin = accessControlAllowOriginValue(req);\n if (!corsOrigin && String(process.env.NODE_ENV || \"\").toLowerCase() === \"production\") {\n const og = String(req.headers?.origin || \"\").trim();\n if (og) {\n console.warn(\"[GATEWAY] genesis SSE: missing Access-Control-Allow-Origin for browser Origin (CORS whitelist mismatch):\", og);\n }\n }\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n res.write(\"retry: 3000\\n\\n\");\n res.write(\": genesis continuity stream\\n\\n\");\n res.write(\"event: genesis\\n\");\n res.write(`data: {\"booted\":true,\"pid\":${process.pid}}\\n\\n`);\n registerGenesisSseClient(res, req);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLatest) {\n const cp = getLatestGenesisSignedCheckpoint();\n if (!cp) {\n sendJson(res, 404, { ok: false, error: \"no_checkpoint_yet\", hint: \"wait_for_seq_interval_or_configure_signing_secret\" });\n return;\n }\n sendJson(res, 200, { ok: true, checkpoint: cp });\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(rhizohRuntime.routes.genesisCheckpointBySeqPrefix)) {\n const tail = pathname.slice(rhizohRuntime.routes.genesisCheckpointBySeqPrefix.length).split(\"/\")[0];\n const seq = parseInt(String(tail || \"\").trim(), 10);\n const out = await genesisCheckpointQueryBySeqV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"checkpoint_not_found\") {\n sendJson(res, 404, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointRange) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryRangeV0(from, to);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"range_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLineage) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const seq = parseInt(String(u.searchParams.get(\"seq\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryLineageV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"lineage_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisContinuityEvents) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const limit = parseInt(String(u.searchParams.get(\"limit\") || \"\").trim(), 10);\n const out = await queryGenesisContinuityEventArchiveV0(from, to, type, limit);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\" || out.error === \"event_archive_disabled\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"archive_file_too_large\") {\n sendJson(res, 413, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplay) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await resolveGenesisReplayRouterV1({\n from: fromSeq,\n to: toSeq,\n type,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayDiff) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const out = await computeGenesisReplayTemporalDiffV1({ from: fromSeq, to: toSeq, type });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEquivalence) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from1 = parseInt(String(u.searchParams.get(\"from1\") || \"\").trim(), 10);\n const to1 = parseInt(String(u.searchParams.get(\"to1\") || \"\").trim(), 10);\n const from2 = parseInt(String(u.searchParams.get(\"from2\") || \"\").trim(), 10);\n const to2 = parseInt(String(u.searchParams.get(\"to2\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await compareGenesisReplayEquivalenceV1({\n from1,\n to1,\n from2,\n to2,\n type,\n includeCheckpoints\n });\n if (\n !out.ok &&\n (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")\n ) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayAnalytics) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayAnalyticsV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEvolution) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const collapseWindows = parseInt(String(u.searchParams.get(\"collapseWindows\") || \"0\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayEvolutionV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n collapseWindows,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/metrics\") {\n res.writeHead(200, { \"Content-Type\": \"text/plain; version=0.0.4; charset=utf-8\" });\n res.end(renderInfraMetricsProm());\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.infraPrometheusMetrics) {\n if (process.env.CASTLE_PROMETHEUS_METRICS !== \"1\") {\n sendJson(res, 404, { ok: false, error: \"prometheus_metrics_disabled\" });\n return;\n }\n res.writeHead(200, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(renderRhizohPrometheusMetrics());\n return;\n }\n if (req.method === \"GET\" && pathname.startsWith(\"/infra/traces/\")) {\n const traceId = decodeURIComponent(pathname.slice(\"/infra/traces/\".length));\n const item = await getTraceById(traceId);\n if (!item) return sendJson(res, 404, { ok: false, error: \"trace_not_found\" });\n return sendJson(res, 200, { ok: true, trace: item });\n }\n if (req.method === \"GET\" && pathname === \"/infra/traces\") {\n const u = new URL(String(req.url || \"/infra/traces\"), `http://${req.headers.host || \"localhost\"}`);\n const sessionId = String(u.searchParams.get(\"sessionId\") || \"\").trim();\n const items = sessionId ? await listTracesBySession(sessionId) : [];\n return sendJson(res, 200, { ok: true, sessionId, traces: items });\n }\n\n /** Presence room mesh (C): JOIN / LEAVE / SNAPSHOT / DELTA / REPLAY (HTTP); SUBSCRIBE = SSE stream. */\n if (req.method === \"POST\" && pathname === \"/presence/mesh/join\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid, clientUid: actor.clientUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/leave\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshLeave(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/snapshot\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n sendJson(res, 200, meshSnapshot(roomUid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/delta\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, MAX_MESSAGE_BYTES);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const seq = meshAppendDelta(roomUid, actor.clientUid, {\n node: body?.node,\n projectionPatch: body?.projectionPatch,\n writerSubject: body?.writerSubject\n });\n sendJson(res, 200, { ok: true, roomUid, seq });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/replay\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const fromNodeId = body?.fromNodeId != null ? String(body.fromNodeId) : \"\";\n const fromSeq = body?.fromSeq;\n if (fromNodeId) {\n sendJson(res, 200, meshReplayFromNodeId(roomUid, fromNodeId));\n return;\n }\n sendJson(res, 200, meshReplayFromSeq(roomUid, fromSeq));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/presence/mesh/subscribe\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const roomUid = String(q.searchParams.get(\"roomUid\") || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n const write = (chunk) => {\n try {\n res.write(chunk);\n } catch {\n /* closed */\n }\n };\n const unsub = meshSubscribeSse(roomUid, write);\n const onClose = () => {\n unsub();\n meshLeave(roomUid, actor.clientUid);\n try {\n res.end();\n } catch {\n /* ignore */\n }\n };\n req.on(\"close\", onClose);\n req.on(\"aborted\", onClose);\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, uid: auth.uid, mode: auth.mode, items: await listConnections(auth.uid) });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const id = await createConnection(auth.uid, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"create_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n await updateConnection(auth.uid, id, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"update_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"DELETE\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n await deleteConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"delete_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections/test\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n if (!checkHttpRateLimit(`llm_conn_test:${auth.uid}`, RL_LLM_CONN_TEST_PER_MIN, 60_000)) {\n return sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n }\n const hasBodyKey = !!String(payload?.apiKey || \"\").trim();\n const result = await queryRhizohLlm(\n {\n message: String(payload?.message || \"Ping from Castle Gateway\"),\n context: payload?.context || { source: \"connection_test\" },\n provider: payload?.provider,\n model: payload?.model,\n apiKey: payload?.apiKey || \"\"\n },\n { allowExternalApiKey: true, llmKeySource: hasBodyKey ? \"auto\" : \"env\" }\n );\n logLlmAccess({\n route: \"/llm/connections/test\",\n uid: auth.uid,\n llmKeyBillingOwner: result.llmKeyBillingOwner,\n llmKeyOrigin: result.llmKeyOrigin,\n provider: result.provider,\n model: result.model,\n connectionId: null\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"connection_test_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAgentIdentities(auth.uid);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerAgentIdentity({\n ownerUid: auth.uid,\n agentId: payload?.agentId,\n role: payload?.role,\n personaSeed: payload?.personaSeed || {},\n capabilityLevel: payload?.capabilityLevel\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/agents/identities/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const owns = await getAgentIdentity(id);\n if (!owns || owns.ownerUid !== auth.uid) return sendJson(res, 403, { ok: false, error: \"forbidden\" });\n const payload = await readHttpJson(req);\n const row = await updateAgentIdentity(id, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/academy/events\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/academy/events\"}`);\n const status = String(url.searchParams.get(\"status\") || \"\");\n const items = await listAcademyEvents({ ownerUid: auth.uid, status, limit: 120 });\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/academy/events\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const ev = await queueAcademyEvent({\n type: payload?.type,\n roomId: payload?.roomId,\n ownerUid: auth.uid,\n participants: payload?.participants || [],\n topic: payload?.topic || \"academy\",\n payload: payload?.payload || {}\n });\n sendJson(res, 200, { ok: true, event: ev });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && String(req.url || \"\").startsWith(\"/academy/events/\") && String(req.url || \"\").endsWith(\"/resolve\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const parts = String(req.url || \"\").split(\"/\").filter(Boolean);\n const eventId = parts[2] || \"\";\n try {\n const payload = await readHttpJson(req);\n const out = await resolveAcademyEvent(eventId, payload || {});\n sendJson(res, 200, { ok: true, event: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_resolve_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/memory\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/memory\"}`);\n const scope = url.searchParams.get(\"scope\") === \"agents\" ? \"agents\" : \"users\";\n const id = String(url.searchParams.get(\"id\") || (scope === \"agents\" ? \"\" : auth.uid));\n const kind = String(url.searchParams.get(\"kind\") || \"\");\n const limit = Number(url.searchParams.get(\"limit\") || 40);\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n sendJson(res, 200, { ok: true, scope, id, kind, items: await listMemories({ scope, id, kind, limit }) });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const profile = await getPersonaGoalMemory(auth.uid);\n sendJson(res, 200, { ok: true, profile });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const profile = await setPersonaGoalMemory({\n uid: auth.uid,\n personaPatch: payload?.personaPatch || {},\n goals: Array.isArray(payload?.goals) ? payload.goals : null,\n preferencesPatch: payload?.preferencesPatch || {}\n });\n sendJson(res, 200, { ok: true, profile });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"profile_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/compact\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const out = await autoCompactMemories({ scope, id });\n sendJson(res, 200, { ok: true, scope, id, ...out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_compact_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/context\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const context = await getMemoryContext({\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n query: String(payload?.query || \"\"),\n limit: Number(payload?.limit || 80)\n });\n sendJson(res, 200, { ok: true, context });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_context_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/place-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const placeName = String(payload?.placeName || \"\").trim();\n if (!placeName) return sendJson(res, 400, { ok: false, error: \"placeName_required\" });\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const persona = String(payload?.waypointPersona || payload?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(placeName)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Place: ${placeName}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Give a concise conversational briefing about this place for a walking-route companion.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-place-brief\", placeName, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `PLACE_BRIEF:${placeName} => ${String(summary.reply || \"\").slice(0, 900)}`,\n tags: [\"place\", \"brief\", \"event-layer\"],\n importance: 0.62,\n meta: { placeName }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"place-brief\",\n text: `${placeName} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: {\n placeName,\n directive: summary.directive,\n lat: hit?.lat ? Number(hit.lat) : null,\n lon: hit?.lon ? Number(hit.lon) : null\n }\n });\n sendJson(res, 200, { ok: true, place: hit || null, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"place_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/route-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const waypoints = Array.isArray(payload?.waypoints) ? payload.waypoints.slice(0, 24) : [];\n const out = [];\n for (const wp of waypoints) {\n const name = String(wp?.name || \"\").trim();\n if (!name) continue;\n const persona = String(wp?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(name)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Waypoint: ${name}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Generate a concise spoken briefing for route waypoint.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-route-brief\", waypoint: name, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n out.push({\n name,\n persona,\n place: hit || null,\n reply: summary.reply,\n directive: summary.directive\n });\n }\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `ROUTE_BRIEF: waypoints=${out.length} ${out.map((x) => `${x.name}:${x.persona}`).join(\" | \")}`.slice(0, 1300),\n tags: [\"route\", \"waypoint\", \"brief\", \"event-layer\"],\n importance: 0.74,\n meta: { count: out.length }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"route-brief\",\n text: out.map((x) => `${x.name}:${String(x.reply || \"\").slice(0, 140)}`).join(\" | \").slice(0, 1400),\n roomId: \"studio-main\",\n meta: {\n waypointCount: out.length,\n waypoints: out.map((x) => ({\n name: x.name,\n persona: x.persona,\n lat: x.place?.lat ? Number(x.place.lat) : null,\n lon: x.place?.lon ? Number(x.place.lon) : null\n }))\n }\n });\n sendJson(res, 200, { ok: true, items: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"route_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/pdf-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const title = String(payload?.title || \"document\");\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const providedText = String(payload?.text || \"\");\n const url = String(payload?.url || \"\");\n let content = providedText;\n if (!content && url) content = await fetchTextFromUrl(url);\n if (!content) return sendJson(res, 400, { ok: false, error: \"text_or_supported_url_required\" });\n const summary = await queryRhizohLlm(\n {\n message: `Summarize this document and provide key points + practical actions.\\nTitle:${title}\\nContent:\\n${content.slice(0, 12000)}`,\n context: { source: \"event-layer-pdf-brief\", title },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `DOC_BRIEF:${title} => ${String(summary.reply || \"\").slice(0, 1000)}`,\n tags: [\"document\", \"brief\", \"library\", \"event-layer\"],\n importance: 0.7,\n meta: { title, url: url || null }\n });\n await addTranscript(auth.uid, {\n source: \"library-brief\",\n eventType: \"pdf-brief\",\n text: `${title} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { title }\n });\n sendJson(res, 200, { ok: true, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/pdf/upload\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req, 8 * 1024 * 1024);\n const job = await enqueuePdfJob({\n uid: auth.uid,\n fileName: payload?.fileName || \"document.pdf\",\n bytesBase64: payload?.contentBase64\n });\n sendJson(res, 200, { ok: true, job });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_upload_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/session\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createPublishSession(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session\",\n text: `Publish session armed: ${row.protocol} ${row.target}`.slice(0, 500),\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: row.id, bridge: row.bridge }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/studio/publish/sessions\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listPublishSessions(auth.uid, 80);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/health-check\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const protocol = String(payload?.protocol || \"WHIP\");\n const target = String(payload?.target || \"\");\n const health = await checkPublishTargetHealth(protocol, target);\n if (payload?.sessionId) {\n await updatePublishSession(auth.uid, String(payload.sessionId), {\n status: health.ok ? \"healthy\" : \"degraded\",\n reconnectPolicy: {\n enabled: true,\n backoffSec: [2, 5, 10, 20],\n nextRetryInSec: health.reconnectInSec || 0,\n maxRetries: 8,\n reason: health.reason\n }\n });\n }\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-health\",\n text: `${protocol} ${target} => ${health.ok ? \"healthy\" : \"degraded\"} (${health.reason})`,\n roomId: \"studio-main\",\n meta: { protocol, target, reconnect: !!health.shouldReconnect }\n });\n sendJson(res, 200, { ok: true, health });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_health_check_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/studio/publish/sessions/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updatePublishSession(auth.uid, id, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session-update\",\n text: `Session ${id} -> ${row.status || \"updated\"}`,\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: id, status: row.status, ingestUrl: row.ingestUrl || \"\" }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/transcripts\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/transcripts\"}`);\n const limit = Math.max(1, Math.min(200, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await listTranscripts(auth.uid, limit);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/transcripts\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await addTranscript(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"transcript_write_failed\" });\n }\n return;\n }\n\n /** E1: broadcast intent → GreenRoom capability → transcript + memory artifact */\n if (req.method === \"POST\" && req.url === \"/studio/capabilities/greenroom\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const traceId = String(payload?.traceId || `TRC-${Date.now().toString(36).toUpperCase()}`);\n const title = String(payload?.title || \"Castle Live Broadcast\").slice(0, 200);\n const intentRaw = String(payload?.intentRaw || \"\").slice(0, 800);\n const audienceEstimate = Math.max(0, Math.min(50_000_000, Number(payload?.audienceEstimate) || 1200));\n const roomId = String(payload?.roomId || \"greenroom-main\");\n const transcriptRow = await addTranscript(auth.uid, {\n source: \"rhizoh\",\n eventType: \"BROADCAST_ROUTED\",\n text: `GreenRoom · ${title}${intentRaw ? `\\nIntent: ${intentRaw}` : \"\"}`,\n roomId,\n meta: {\n ack: \"BROADCAST_ROUTED\",\n capability: \"greenroom\",\n intentType: \"broadcast\",\n traceId,\n audienceEstimate,\n artifact: {\n kind: \"broadcast_card\",\n title,\n traceId,\n roomId,\n createdAt: Date.now()\n }\n }\n });\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n text: `GREENROOM_BROADCAST:${title} trace=${traceId}`,\n tags: [\"broadcast\", \"greenroom\", \"artifact\", \"castle-library\"],\n importance: 0.72,\n kind: \"episodic\",\n meta: { traceId, capability: \"greenroom\", transcriptId: transcriptRow.id }\n });\n sendJson(res, 200, {\n ok: true,\n ack: \"BROADCAST_ROUTED\",\n traceId,\n transcript: transcriptRow,\n replayPath: `/replay/${encodeURIComponent(traceId)}`,\n sharePath: `/replay/${encodeURIComponent(traceId)}`\n });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"greenroom_capability_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/metadata/search\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/metadata/search\"}`);\n const q = String(url.searchParams.get(\"q\") || \"\");\n const limit = Math.max(1, Math.min(300, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await searchMetadata(auth.uid, q, limit);\n sendJson(res, 200, { ok: true, q, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerDevice(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, device: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"device_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const lat = Number(payload?.lat);\n const lon = Number(payload?.lon);\n const speed = Math.max(0, Number(payload?.speed || 0));\n const cfgCenterLat = Number(process.env.CASTLE_GEOFENCE_CENTER_LAT || 41.0082);\n const cfgCenterLon = Number(process.env.CASTLE_GEOFENCE_CENTER_LON || 28.9784);\n const cfgRadiusM = Math.max(100, Number(process.env.CASTLE_GEOFENCE_RADIUS_M || 55000));\n const cfgSpeedCap = Math.max(1, Number(process.env.CASTLE_SPEED_CAP_MPS || 35));\n const dist = Number.isFinite(lat) && Number.isFinite(lon) ? haversineMeters(cfgCenterLat, cfgCenterLon, lat, lon) : 0;\n const geofenceOk = !Number.isFinite(dist) ? true : dist <= cfgRadiusM;\n const speedOk = speed <= cfgSpeedCap;\n const failsafe = !(geofenceOk && speedOk);\n const safety = {\n geofenceOk,\n speedOk,\n failsafe,\n distanceFromCenterM: Number.isFinite(dist) ? dist : null,\n speedCapMps: cfgSpeedCap\n };\n const row = await appendTelemetry(auth.uid, deviceId, {\n lat,\n lon,\n speed,\n battery: payload?.battery,\n mode: payload?.mode || (failsafe ? \"FAILSAFE_HOLD\" : \"ACTIVE\"),\n raw: payload?.raw || {}\n });\n await updateDevice(auth.uid, deviceId, {\n status: failsafe ? \"failsafe\" : \"active\",\n lastTelemetryAt: row.ts,\n safetyState: safety\n });\n if (failsafe) {\n await addTranscript(auth.uid, {\n source: \"robotics-guardrails\",\n eventType: \"failsafe\",\n text: `Device ${deviceId} failsafe triggered geofenceOk=${geofenceOk} speedOk=${speedOk}`,\n roomId: \"studio-main\",\n meta: { deviceId, safety }\n });\n }\n sendJson(res, 200, { ok: true, telemetry: row, safety, command: failsafe ? \"FAILSAFE_HOLD\" : \"CONTINUE\" });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"telemetry_ingest_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listTelemetry(auth.uid, 150);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createAutomation(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"automation-scheduled\",\n text: `Automation scheduled: ${row.title} at ${new Date(row.scheduleAt).toLocaleString()}`,\n roomId: \"studio-main\",\n meta: { automationId: row.id, action: row.action, deviceId: row.deviceId }\n });\n sendJson(res, 200, { ok: true, automation: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"automation_create_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAutomations(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listDevices(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const devs = await listDevices(auth.uid, 500);\n const d = devs.find((x) => x.id === deviceId);\n if (!d) return sendJson(res, 404, { ok: false, error: \"device_not_found\" });\n const action = String(payload?.action || \"\");\n const caps = Array.isArray(d.capabilityProfile) ? d.capabilityProfile : [];\n if (action && caps.length && !caps.includes(action) && !caps.includes(\"*\")) {\n return sendJson(res, 400, { ok: false, error: \"unsupported_action_for_device\", capabilityProfile: caps });\n }\n const cmd = await enqueueCommand(auth.uid, {\n deviceId,\n action: action || \"NOOP\",\n params: payload?.params || {},\n adapter: payload?.adapter || d.adapter || \"websocket\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-command-queue\",\n eventType: \"command-queued\",\n text: `${cmd.deviceId} -> ${cmd.action}`,\n roomId: \"studio-main\",\n meta: { commandId: cmd.id, adapter: cmd.adapter }\n });\n sendJson(res, 200, { ok: true, command: cmd });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listCommands(auth.uid, 200);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/robotics/commands/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updateCommandStatus(auth.uid, id, payload?.status || \"sent\");\n sendJson(res, 200, { ok: true, command: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/rhizoh/bridge\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const result = await runRhizohBrainV2({\n message: String(payload?.message || \"robotics bridge command\"),\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n worldState: payload?.worldState || {},\n deviceState: payload?.deviceState || {},\n intent: \"robotics-mechanics\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-bridge\",\n eventType: \"robotics-command\",\n text: `Bridge -> ${String(result.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { directive: result.directive, agentId: payload?.agentId || \"\" }\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"robotics_bridge_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/planner/graph\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const graph = buildTaskActionGraph(String(payload?.task || \"\"), payload?.context || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"planner-graph\",\n text: `Planner graph built: ${graph.taskTitle} nodes=${graph.nodes.length}`,\n roomId: \"studio-main\",\n meta: { taskTitle: graph.taskTitle, nodeCount: graph.nodes.length }\n });\n sendJson(res, 200, { ok: true, graph });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"planner_graph_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerSocialChannel(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, channel: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_channel_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listSocialChannels(auth.uid, 100);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/broadcast\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const text = String(payload?.text || \"\").slice(0, 1200);\n const channels = await listSocialChannels(auth.uid, 100);\n const enabled = channels.filter((c) => c.status === \"enabled\");\n const deliveries = [];\n for (const c of enabled) {\n const out = await deliverSocialWithRetry(c, text);\n deliveries.push({ id: c.id, platform: c.platform, ...out });\n }\n const okCount = deliveries.filter((d) => d.ok).length;\n await addTranscript(auth.uid, {\n source: \"social-layer\",\n eventType: \"social-broadcast\",\n text: `Broadcast delivered ${okCount}/${enabled.length} channel(s): ${text.slice(0, 240)}`,\n roomId: \"studio-main\",\n meta: { deliveries }\n });\n sendJson(res, 200, { ok: true, sentChannels: okCount, totalChannels: enabled.length, deliveries });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_broadcast_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/ethics/programs\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, ...buildEthicsPrograms() });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands/dispatch\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const all = await listCommands(auth.uid, 200);\n const queued = all.filter((c) => String(c.status || \"queued\") === \"queued\").slice(0, 50);\n const devices = await listDevices(auth.uid, 500);\n const byId = new Map(devices.map((d) => [d.id, d]));\n const results = [];\n for (const cmd of queued) {\n const dev = byId.get(cmd.deviceId);\n if (!dev) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: \"device_not_found\" });\n continue;\n }\n try {\n const dispatched = await dispatchCommandToAdapter(dev, cmd);\n await updateCommandStatus(auth.uid, cmd.id, \"sent\");\n results.push({ id: cmd.id, ok: true, adapter: dispatched.adapter, via: dispatched.via });\n } catch (error) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: String(error?.message || \"dispatch_failed\") });\n }\n }\n await addTranscript(auth.uid, {\n source: \"robotics-command-dispatch\",\n eventType: \"command-dispatch-batch\",\n text: `Dispatch batch size=${results.length} ok=${results.filter((x) => x.ok).length}`,\n roomId: \"studio-main\",\n meta: { results: results.slice(0, 30) }\n });\n sendJson(res, 200, { ok: true, processed: results.length, results });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"dispatch_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/pdf/jobs/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n const job = await getPdfJob(auth.uid, id);\n if (!job) return sendJson(res, 404, { ok: false, error: \"pdf_job_not_found\" });\n sendJson(res, 200, { ok: true, job });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const row = await appendMemory({\n scope,\n id,\n text: payload?.text,\n tags: payload?.tags || [],\n importance: payload?.importance,\n kind: payload?.kind || \"episodic\",\n meta: { uid: auth.uid, ...(payload?.meta || {}) }\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.externalTruth) {\n try {\n if (process.env.RHIZOH_EXTERNAL_TRUTH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_truth:${ip}`, RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n sendJson(res, 200, buildRhizohExternalGroundTruthPayload(req));\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.productOutcome) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const rawBody = await readHttpBodyText(req, 64 * 1024);\n const body = rawBody.trim() ? safeJsonParse(rawBody) : null;\n if (!body || typeof body !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"invalid_json_payload\" });\n return;\n }\n const sigHdr = String(req.headers?.[\"x-rhizoh-outcome-signature\"] || \"\").trim();\n const hmacV = verifyRhizohOutcomeHmac(rawBody, sigHdr);\n if (!hmacV.ok) {\n sendJson(res, 401, { ok: false, error: hmacV.error || \"outcome_hmac_invalid\" });\n return;\n }\n const srcTokHdr = String(req.headers?.[\"x-rhizoh-outcome-source-token\"] || \"\").trim();\n const srcV = verifyRhizohOutcomeSourceToken(body.source, srcTokHdr);\n if (!srcV.ok) {\n sendJson(res, 401, { ok: false, error: srcV.error || \"outcome_source_token_invalid\" });\n return;\n }\n const subjectKey = String(body.subjectKey || \"\").trim().slice(0, 128);\n if (\n subjectKey &&\n !checkHttpRateLimit(\n `rhizoh_product_outcome_subj:${subjectKey}`,\n RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN,\n 60_000\n )\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_subject\" });\n return;\n }\n const result = ingestRhizohProductOutcomeHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n schemaVersion: \"1.1.0\",\n aggregate: result.aggregate,\n integrity: result.integrity\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"outcome_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.productOutcomeAggregate) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome_agg:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const u = new URL(String(req.url || \"/\"), \"http://localhost\");\n const cohortId = u.searchParams.get(\"cohortId\") || \"\";\n const decisionFingerprint = u.searchParams.get(\"decisionFingerprint\") || \"\";\n const limit = u.searchParams.get(\"limit\") || \"50\";\n const rows = listRhizohProductOutcomeAggregates({\n cohortId: cohortId || undefined,\n decisionFingerprint: decisionFingerprint || undefined,\n limit: Number(limit) || 50\n });\n sendJson(res, 200, { ok: true, schemaVersion: \"1.1.0\", rows });\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.externalLossBatch) {\n try {\n if (process.env.RHIZOH_EXTERNAL_LOSS_BATCH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_loss_batch:${ip}`, RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const result = ingestRhizohExternalLossBatchHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, { ok: true, accepted: result.accepted ?? 0 });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"batch_failed\") });\n }\n return;\n }\n\n function moderationAdminOk(req) {\n const expected = String(process.env.CASTLE_MODERATION_ADMIN_KEY || \"\").trim();\n if (!expected) return false;\n const got = String(req.headers[\"x-castle-moderation-key\"] || \"\").trim();\n return got.length > 0 && got === expected;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.opsAbuseReport) {\n try {\n const body = await readHttpJson(req, 32 * 1024);\n const auth = await resolveHttpUser(req);\n const entry = submitAbuseReportV0({\n reporterUid: auth.ok ? auth.uid : null,\n targetUid: body?.targetUid ?? null,\n category: body?.category,\n detail: body?.detail,\n traceId: body?.traceId ?? null\n });\n sendJson(res, 202, { ok: true, reportId: entry.id, status: entry.status });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsModerationQueue) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n queue: listModerationQueueV0(100),\n softBlocked: listSoftBlockedUsersV0()\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-block\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const out = softBlockUserV0(body?.uid, body?.reason);\n sendJson(res, out.ok ? 200 : 400, out);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-unblock\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n sendJson(res, 200, removeSoftBlockV0(body?.uid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsAgentSnapshots) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n const limit = Number(new URL(req.url || \"\", \"http://local\").searchParams.get(\"limit\") || 50);\n sendJson(res, 200, { ok: true, snapshots: listRecentAgentSnapshotsV0(limit) });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.academicObservatoryExport) {\n const auth = await resolveHttpUser(req);\n const out = handleAcademicObservatoryExportGetV0(req, auth);\n sendJson(res, out.status, out.body);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsHardeningStatus) {\n const auth = await resolveHttpUser(req);\n const principal = auth.ok ? `uid:${auth.uid}` : null;\n const gclSnap = await getGlobalCostLedgerSnapshotV0(principal);\n const economicStrategy = evaluateEconomicStrategyL3V0(gclSnap);\n const hardeningTenantId = resolveHardeningTenantIdV0(auth, req);\n const unifiedState = await buildUnifiedStateNarrativeV0({\n tenantId: hardeningTenantId ?? undefined,\n platformScope: !hardeningTenantId,\n principal\n });\n sendJson(res, 200, {\n ok: true,\n schema: \"rhizoh.ops.hardening_status.v0\",\n containment: readAgentContainmentConfigV0(),\n cost: readCostContainmentConfigV0(),\n gcl: {\n backend: await getGclBackendStatusV0(),\n snapshot: gclSnap\n },\n economicStrategyL3: economicStrategy,\n phasedRollout: await getPhasedRolloutStatsV0(),\n tier: readPhasedRolloutTierV0(),\n principalCost: auth.ok ? await getCostContainmentStatsV0(principal) : null,\n stressTaxonomy: verifyStressTaxonomyCoverageV0(),\n stressConflictResolution: verifyStressConflictResolutionV0(),\n resolutionStabilityEnvelope: verifyResolutionStabilityEnvelopeV0(),\n unifiedState: {\n tenantScope: {\n tenantId: unifiedState.tenantScope?.tenantId,\n scope: unifiedState.tenantScope?.scope,\n noGlobalDerivedState: unifiedState.tenantScope?.noGlobalDerivedState\n },\n narrativeFingerprint: unifiedState.narrativeFingerprint,\n screenshotScopeWatermark: unifiedState.screenshotScopeWatermark,\n interpretationSafetyContract: unifiedState.interpretationSafetyContract,\n interpretationUxContract: {\n schema: unifiedState.interpretationUxContract?.schema,\n layerOrder: unifiedState.interpretationUxContract?.layerOrder,\n narrativeNeverVisuallyDominant: unifiedState.interpretationUxContract?.narrativeNeverVisuallyDominant\n },\n governance: {\n narrativeIsNotDecisionLayer: unifiedState.governance.narrativeIsNotDecisionLayer,\n displayOrder: unifiedState.governance.displayOrder,\n trustPosture: unifiedState.governance.trustPosture,\n interpretationUxContract: unifiedState.governance.interpretationUxContract?.schema\n },\n humanOps: {\n decisionTier: unifiedState.humanOps?.humanDecisionScaling?.tier,\n decisionOwner: unifiedState.humanOps?.humanDecisionScaling?.decisionOwner,\n opsRouteId: unifiedState.humanOps?.humanDecisionOpsRunbook?.routing?.routeId,\n slaAckMinutes: unifiedState.humanOps?.humanDecisionOpsRunbook?.sla?.ackMinutes,\n uiDisclaimerRequired: unifiedState.humanOps?.narrativeUiSafety?.displayRules?.requireDisclaimerBanner,\n misreadHighResidual: unifiedState.humanOps?.misreadSimulation?.highResidualCount,\n propagationHighResidual: unifiedState.humanOps?.socialPropagationSimulation?.highResidualCount,\n worstWatermarkSurvivability:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.worstWatermarkSurvivability,\n dominantDistortionSource:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.dominantDistortionSource\n },\n culturalRisk: {\n perceptualRiskClass: unifiedState.culturalRisk?.perceptualRiskClass,\n trustFork: unifiedState.culturalRisk?.trustDynamics?.fork,\n trustDecayScore: unifiedState.culturalRisk?.trustDynamics?.scores?.trustDecay,\n mythologyScore: unifiedState.culturalRisk?.trustDynamics?.scores?.mythology,\n confidenceDetachmentExposure:\n unifiedState.culturalRisk?.trustDynamics?.axes?.confidenceDetachmentExposure\n },\n appliedSystemsLayer: {\n schema: unifiedState.appliedSystemsLayer?.schema,\n researchOutputType: unifiedState.appliedSystemsLayer?.researchGovernance?.outputType,\n roboticsBlockActuation: unifiedState.appliedSystemsLayer?.roboticsGrounding?.blockActuation,\n spiralRumorCount:\n unifiedState.appliedSystemsLayer?.spiralMMOGameKernel?.gameLayer?.rumors?.length\n },\n actionSemanticGovernance: {\n schema: unifiedState.actionSemanticGovernance?.schema,\n highRiskActionCount: unifiedState.actionSemanticGovernance?.highRiskActionCount,\n loopClosed: unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.loopClosed,\n authorityCollapseMitigated:\n unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.risks?.authorityCollapse\n ?.mitigated\n },\n actionContextResolution: {\n schema: unifiedState.actionContextResolution?.schema,\n contextFingerprint: unifiedState.actionContextResolution?.contextFingerprint?.fingerprintHash,\n domainCollision: unifiedState.actionContextResolution?.domainCollision?.detected,\n deployAgentSpiralBlocked:\n unifiedState.actionContextResolution?.actionOverloadingRisk?.deployAgentExample\n ?.spiral_mmo === \"blocked\",\n roboticsActuationEligible:\n unifiedState.actionContextResolution?.roboticsActuationCorrectness?.actuationEligible\n },\n epistemicCoherence: {\n schema: unifiedState.epistemicCoherence?.schema,\n verdict: unifiedState.epistemicCoherence?.systemCoherence?.verdict,\n coherenceScore: unifiedState.epistemicCoherence?.systemCoherence?.score,\n contradictionCount: unifiedState.epistemicCoherence?.crossLayerContradictions?.count,\n operatorDecision: unifiedState.epistemicCoherence?.uxCompression?.operatorDecision,\n headlineTr: unifiedState.epistemicCoherence?.uxCompression?.headline?.tr,\n falseSafetyWarning: Boolean(\n unifiedState.epistemicCoherence?.uxCompression?.falseSafetyWarning\n )\n },\n coherenceAuthorityBoundary: {\n schema: unifiedState.coherenceAuthorityBoundary?.schema,\n bindingValid: unifiedState.coherenceAuthorityBoundary?.eclAuthorityBinding?.valid,\n canExecute: unifiedState.coherenceAuthorityBoundary?.contract?.can_execute,\n mandatoryBannerTr:\n unifiedState.coherenceAuthorityBoundary?.disclaimers?.mandatoryBanner?.tr\n },\n decisionLatencyGovernance: {\n schema: unifiedState.decisionLatencyGovernance?.schema,\n latencyTier: unifiedState.decisionLatencyGovernance?.latencyTier,\n inflationScore: unifiedState.decisionLatencyGovernance?.latencyInflation?.score,\n fastPathEligible: unifiedState.decisionLatencyGovernance?.fastPath?.eligible,\n primaryActionId: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.primaryActionId,\n ackSlaMinutes: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.ackSlaMinutes,\n certaintyCap: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.certaintyCap,\n uncertaintyItems:\n unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.uncertaintyEnvelope?.itemCount\n },\n decisionPacketUncertaintyBoundary: {\n schema: unifiedState.decisionPacketUncertaintyBoundary?.schema,\n invariantValid: unifiedState.decisionPacketUncertaintyBoundary?.invariantCheck?.valid,\n centralInvariant: unifiedState.decisionPacketUncertaintyBoundary?.centralInvariant\n },\n epistemicDecisionPacing: {\n schema: unifiedState.epistemicDecisionPacing?.schema,\n queueSaturationTier:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation?.tier,\n operatorProcessingLatency:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation\n ?.operatorProcessingLatency,\n postureWindowCount:\n unifiedState.epistemicDecisionPacing?.temporaryStaticPostureWindows\n ?.temporaryStaticPostureWindows?.windowCount,\n executionBoundariesValid:\n unifiedState.epistemicDecisionPacing?.executionBoundaries?.valid,\n grantsExecutionApproval: unifiedState.epistemicDecisionPacing?.grantsExecutionApproval\n },\n epistemicTemporalCoherence: {\n schema: unifiedState.epistemicTemporalCoherence?.schema,\n allInvariantsValid: unifiedState.epistemicTemporalCoherence?.allInvariantsValid,\n temporalAlignmentValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.temporalAlignment?.valid,\n crossWindowGuardValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard?.valid,\n delayedTruthValid: unifiedState.epistemicTemporalCoherence?.invariants?.delayedTruthRule?.valid,\n reconciliationRequired:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard\n ?.reconciliation?.required\n },\n realityDriftObserver: {\n schema: unifiedState.realityDriftObserver?.schema,\n driftActive: unifiedState.realityDriftObserver?.driftObserverSummary?.active,\n misapprehensionShapeCount:\n unifiedState.realityDriftObserver?.misapprehensionShapeCatalog?.shapeCount,\n criticalShapeCount:\n unifiedState.realityDriftObserver?.driftObserverSummary?.criticalShapeCount,\n roboticsMismatchActive:\n unifiedState.realityDriftObserver?.roboticsFeedbackMismatch?.active\n },\n driftCausality: {\n schema: unifiedState.driftCausality?.schema,\n shapesWithCausalExplanation: unifiedState.driftCausality?.rdolLinkage?.shapesWithCausalExplanation,\n activeDomainCount: unifiedState.driftCausality?.causalitySummary?.activeDomainCount,\n propagationCausalityActive: unifiedState.driftCausality?.causalDomains?.propagation?.active\n },\n productReadiness: unifiedState.productReadiness,\n stateLayers: {\n raw: unifiedState.stateLayers.raw,\n derived: {\n systemState: unifiedState.stateLayers.derived.systemState,\n narrative: unifiedState.stateLayers.derived.narrative,\n validation: unifiedState.stateLayers.derived.validation\n },\n policy: {\n binding: unifiedState.stateLayers.policy.binding,\n decisionOwner: unifiedState.stateLayers.policy.decisionOwner,\n prohibitedActions: unifiedState.stateLayers.policy.prohibitedActions\n }\n },\n systemState: unifiedState.systemState,\n interpretation: unifiedState.interpretation,\n validation: {\n trustPosture: unifiedState.validation.trustPosture,\n adjustedConfidence: unifiedState.validation.adjustedConfidence,\n agreement: unifiedState.validation.confidenceDecomposition.agreement,\n divergenceScore: unifiedState.validation.divergence.divergenceScore,\n validated: unifiedState.validation.divergence.validated,\n uncertaintyTags: unifiedState.validation.uncertainty.tags.map((t) => t.id),\n flags: unifiedState.validation.divergence.flags.map((f) => f.id)\n },\n compression: unifiedState.compression\n }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 6 * 1024 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (\n !checkHttpRateLimit(`rhizoh_voice_transcribe:${rlKey}`, RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN, 60_000)\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const voiceEnv = rhizohVoiceTranscribeEnvV3();\n if (!voiceEnv.any) {\n sendJson(res, 503, {\n ok: false,\n error: \"voice_asr_not_configured\",\n env: voiceEnv\n });\n return;\n }\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n const result = await runRhizohVoiceTranscribeV3(\n applyLanguagePropagationToVoiceBodyV1(payload, langProp)\n );\n sendJson(res, result.ok ? 200 : 422, {\n ...result,\n traceId: String(payload?.traceId || \"\"),\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n atMs: Date.now()\n });\n } catch (error) {\n sendJson(res, 500, {\n ok: false,\n error: \"voice_transcribe_failed\",\n detail: String(error?.message || error),\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === rhizohRuntime.routes.rhizohLlm) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 128 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n logRhizohHealth(\"gateway_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: String(payload?.traceId || \"\"),\n auth: auth.ok ? \"ok\" : \"anon\",\n ip\n });\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (!checkHttpRateLimit(`rhizoh_llm:${rlKey}`, RL_RHIZOH_LLM_PER_MIN, 60_000)) {\n const rlTaxonomy = classifyStressResponseV0({ code: \"rate_limit_exceeded\" });\n return sendJson(res, 429, {\n ok: false,\n error: \"rate_limit_exceeded\",\n reply: rlTaxonomy.userMessageTr || \"İstek sınırı aşıldı. Kısa süre sonra tekrar deneyin.\",\n stressClass: rlTaxonomy.stressClass,\n responseAction: rlTaxonomy.responseAction,\n stressMatrix: rlTaxonomy.matrix,\n stressInterpretable: rlTaxonomy.interpretable,\n stressConfidence: rlTaxonomy.stressConfidence,\n actionConfidence: rlTaxonomy.actionConfidence,\n actionSoftened: rlTaxonomy.actionSoftened,\n responseActionStrict: rlTaxonomy.responseActionStrict,\n actionInterpretable: rlTaxonomy.actionInterpretable,\n conflictResolution: rlTaxonomy.conflictResolution,\n directive: \"NONE\"\n });\n }\n\n const requireExplicit = process.env.CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE === \"1\";\n const keyMode = normalizeClientLlmKeySource(payload?.llmKeySource ?? payload?.keySource);\n if (keyMode === null) {\n return sendJson(res, 400, {\n ok: false,\n error: \"invalid_llm_key_source\",\n reply: \"llmKeySource geçersiz. Kullanın: env | user_connection | auto\",\n directive: \"NONE\"\n });\n }\n if (requireExplicit && keyMode === \"auto\") {\n return sendJson(res, 400, {\n ok: false,\n error: \"llm_key_source_required\",\n reply: \"Sunucu CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE=1: istekte llmKeySource (env veya user_connection) zorunlu.\",\n directive: \"NONE\"\n });\n }\n\n const conn = auth.ok ? await resolveConnection(auth.uid, String(payload?.connectionId || \"\")) : null;\n\n if (keyMode === \"user_connection\") {\n if (!auth.ok) {\n return sendJson(res, 401, {\n ok: false,\n error: \"llm_auth_required_for_user_connection\",\n reply: \"Kayıtlı LLM bağlantısı için giriş gerekli.\",\n directive: \"NONE\"\n });\n }\n if (!conn?.apiKey) {\n return sendJson(res, 400, {\n ok: false,\n error: \"user_llm_connection_required\",\n reply: \"Geçerli bir LLM bağlantısı yok; Studio’da bağlantı ekleyin veya llmKeySource=env kullanın.\",\n directive: \"NONE\"\n });\n }\n }\n\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n let safePayload = applyLanguagePropagationToLlmPayloadV1({ ...(payload || {}) }, langProp);\n const trimOut = trimRhizohLlmGatewayPayloadV1(safePayload);\n if (trimOut.meta.trimmed) {\n logRhizohHealth(\"llm_payload_trimmed\", {\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n bytesBefore: trimOut.meta.bytesBefore,\n bytesAfter: trimOut.meta.bytesAfter\n });\n }\n safePayload = trimOut.payload;\n delete safePayload.apiKey;\n delete safePayload.llmKeySource;\n delete safePayload.keySource;\n\n let resolvedProvider = payload?.provider;\n let resolvedModel = payload?.model;\n let connApiKey = \"\";\n\n if (keyMode === \"user_connection\") {\n resolvedProvider = conn.provider;\n resolvedModel = conn.model;\n connApiKey = conn.apiKey || \"\";\n } else if (keyMode === \"env\") {\n connApiKey = \"\";\n } else {\n resolvedProvider = conn?.provider || payload?.provider;\n resolvedModel = conn?.model || payload?.model;\n connApiKey = conn?.apiKey || \"\";\n }\n\n const { result, traceId, turnLatencyMs, spinePhases, sampledTrace } = await rhizohGatewayTurn({\n safePayload,\n auth,\n keyMode,\n conn,\n resolvedProvider,\n resolvedModel,\n connApiKey\n });\n logRhizohHealth(\"llm_response\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId,\n turnLatencyMs: Number(turnLatencyMs || 0),\n keyMode\n });\n sendJson(res, 200, {\n ...result,\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n traceId,\n turnLatencyMs,\n ...(spinePhases ? { spinePhases } : {}),\n ...(sampledTrace !== undefined ? { sampledTrace } : {})\n });\n } catch (error) {\n const msg = String(error?.message || \"\");\n const code = error?.code || \"\";\n let status = 500;\n if (msg === \"payload_too_large\") status = 413;\n else if (msg === \"rate_limit_exceeded\") status = 429;\n else if (\n code === \"cost_hard_limit\" ||\n code === \"phased_rollout_capacity\" ||\n error?.containment === true ||\n String(code).startsWith(\"agent_\")\n ) {\n status = 429;\n } else if (code === \"user_soft_blocked\" || code === \"prompt_abuse_detected\") status = 403;\n else if ([\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(code) || msg.includes(\"missing_api_key_for_\"))\n status = 400;\n let rhizohFailureKind = \"provider_error\";\n if (msg === \"rate_limit_exceeded\" || status === 429) rhizohFailureKind = \"rate_limit\";\n else if (status === 403) rhizohFailureKind = \"policy_block\";\n else if (status === 400 || status === 413) rhizohFailureKind = \"client_config\";\n else if (msg.startsWith(\"provider_http_\")) {\n const st = Number(msg.replace(\"provider_http_\", \"\"));\n if (st === 408 || st === 504) rhizohFailureKind = \"timeout\";\n else rhizohFailureKind = \"provider_error\";\n } else if (/timeout|timed out|ETIMEDOUT/i.test(msg)) rhizohFailureKind = \"timeout\";\n\n const providerHttpMatch = msg.match(/^provider_http_(\\d{3})$/);\n const stressTaxonomy =\n error?.stressClass != null\n ? {\n stressClass: error.stressClass,\n responseAction: error.responseAction,\n stressMatrix: error.stressMatrix,\n stressInterpretable: error.stressInterpretable !== false,\n stressConfidence: error.stressConfidence,\n actionConfidence: error.actionConfidence,\n actionSoftened: error.actionSoftened,\n responseActionStrict: error.responseActionStrict,\n actionInterpretable: error.actionInterpretable,\n conflictResolution: error.conflictResolution,\n stressSecondary: error.stressSecondary,\n responseActions: error.responseActions\n }\n : (() => {\n const t = classifyStressResponseV0({\n code: code || msg,\n rhizohFailureKind,\n providerHttpStatus: providerHttpMatch\n ? Number(providerHttpMatch[1])\n : null\n });\n return {\n stressClass: t.stressClass,\n responseAction: t.responseAction,\n stressMatrix: t.matrix,\n stressInterpretable: t.interpretable,\n stressConfidence: t.stressConfidence,\n actionConfidence: t.actionConfidence,\n actionSoftened: t.actionSoftened,\n responseActionStrict: t.responseActionStrict,\n actionInterpretable: t.actionInterpretable,\n conflictResolution: t.conflictResolution,\n stressSecondary: t.stressSecondary,\n responseActions: t.responseActions\n };\n })();\n sendJson(res, status, {\n ok: false,\n rhizohFailureKind,\n ...(providerHttpMatch ? { providerHttpStatus: Number(providerHttpMatch[1]) } : {}),\n error: code || msg || \"rhizoh_llm_failed\",\n detail: msg || code || undefined,\n reply:\n error?.reply ||\n (status === 500 ? \"Rhizoh bağlantısı geçici olarak kesildi.\" : msg),\n directive: error?.directive || \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n ...stressTaxonomy\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicSeal) {\n try {\n if (!EPISTEMIC_SEAL_SECRET) {\n sendJson(res, 503, { ok: false, error: \"epistemic_seal_disabled\" });\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`epistemic_seal:${ip}`, RL_EPISTEMIC_SEAL_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const truth = body.truth_contract;\n if (!truth || typeof truth !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"truth_contract_required\" });\n return;\n }\n const sealBody = {\n truth_contract: truth,\n runtime_hash: String(body.runtime_hash || \"\"),\n model_route: {\n provider: body.model_route?.provider ?? null,\n model: body.model_route?.model ?? null\n },\n memory_digest: String(body.memory_digest || \"\"),\n world_snapshot_hash: String(body.world_snapshot_hash || \"\"),\n timestamp: Number(body.timestamp) || Date.now()\n };\n const canonical = canonicalEpistemicSealString(sealBody);\n const { hash, signature } = hashAndSignEpistemicSeal(canonical, EPISTEMIC_SEAL_SECRET);\n recordGenesisEpistemicSealIssued(hash);\n const attestation = {\n schema: EPISTEMIC_SEAL_SCHEMA,\n algorithm: \"SHA-256+HMAC-SHA256\",\n keyHint: process.env.CASTLE_EPISTEMIC_SEAL_SECRET ? \"CASTLE_EPISTEMIC_SEAL_SECRET\" : \"CASTLE_GATEWAY_TOKEN\",\n issuedAt: Date.now(),\n gateway: { service: \"castle-gateway\", listenPort: PORT }\n };\n sendJson(res, 200, {\n ok: true,\n hash,\n signature,\n attestation,\n canonicalBytes: Buffer.byteLength(canonical, \"utf8\")\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"seal_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicLogsBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`epistemic_logs:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 120) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const persisted = await persistEpistemicLedgerBatch(auth.uid, entries);\n recordGenesisEpistemicLedgerPersisted(persisted.persisted);\n // Internal cognition layer: forecast stays gateway/ledger-only (no client exposure).\n try {\n await persistEpistemicForecastBatch(auth.uid, persisted.normalized || []);\n } catch {\n /* forecast failures must not break primary ledger ingest */\n }\n sendJson(res, 200, {\n ok: true,\n persisted: persisted.persisted,\n mode: persisted.mode,\n latest: persisted.normalized?.[persisted.normalized.length - 1] || null\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"epistemic_log_ingest_failed\") });\n }\n return;\n }\n\n sendJson(res, 404, { ok: false, error: \"not_found\" });\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\nspiralWssForGenesis = wss;\nconst orchestrator = createOrchestrator();\nconst clientStats = new Map();\nlet broadcasterClientId = null;\nconst spiralState = {\n rooms: [{ id: \"room-default\", name: \"Spiral Default Arena\" }],\n activeByClient: new Map(),\n characters: new Map() // id -> {id,name,role,roomId,source}\n};\n\n/** Castle social rooms — keyed by `castleRoomKey` from {@link WS_MESSAGE.CASTLE_SOCIAL_PULSE}. */\nconst castleSocialRoomState = new Map();\nconst CASTLE_SOCIAL_MEMBER_TTL_MS = 90_000;\nconst CASTLE_SOCIAL_MAX_PULSE_BYTES = 8192;\nconst RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES = 5 * 1024 * 1024;\nconst RHIZOH_VOICE_LIVE_MAX_CHUNKS = 96;\n\n/**\n * Voice Engine v3 live media lane.\n * Client streams MediaRecorder chunks over the existing gateway WS; STOP reuses the\n * gateway ASR runner so orchestration remains the single authority downstream.\n */\nconst rhizohVoiceLiveSessions = new WeakMap();\n\nfunction appendRhizohVoiceLiveEventV0(session, type, payloadRefSeed = \"\") {\n if (!session) return null;\n session.eventSeq = (Number(session.eventSeq) || 0) + 1;\n const event = createRhizohImmutableEventV0({\n type,\n sessionId: session.sessionId,\n traceId: session.traceId,\n actorSeed: session.sessionId || session.traceId,\n eventSeq: session.eventSeq,\n payloadRef: createRhizohPayloadRefV0(payloadRefSeed || `${session.sessionId}:${type}:${session.eventSeq}`)\n });\n session.events = [...(session.events || []), event].slice(-64);\n return event;\n}\n\nfunction sendRhizohVoiceLiveEnvelopeV0(socket, type, payload) {\n try {\n if (socket?.readyState === 1) socket.send(JSON.stringify(createEnvelope(type, payload)));\n } catch {\n /* noop */\n }\n}\n\nfunction decodeRhizohVoiceLiveChunkV0(payload = {}) {\n const b64 = String(payload.audioBase64 || \"\").trim();\n if (!b64) return { ok: false, error: \"audio_base64_required\" };\n try {\n const buf = Buffer.from(b64, \"base64\");\n if (!buf.length) return { ok: false, error: \"audio_decode_empty\" };\n return { ok: true, buffer: buf };\n } catch {\n return { ok: false, error: \"audio_base64_invalid\" };\n }\n}\n\nfunction handleRhizohVoiceLiveStartV0(socket, payload = {}) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const sessionId = String(p.sessionId || `live_${Date.now().toString(36)}`).slice(0, 96);\n rhizohVoiceLiveSessions.set(socket, {\n sessionId,\n traceId: String(p.traceId || \"\").slice(0, 128),\n mimeType: String(p.mimeType || \"audio/webm\").slice(0, 80),\n languageCode: String(p.languageCode || \"tr-TR\").slice(0, 32),\n path: String(p.path || \"both\").slice(0, 24),\n chunks: [],\n chunkIndexes: [],\n totalBytes: 0,\n nextChunkIndex: 1,\n eventSeq: 0,\n events: [],\n startedAtMs: Date.now()\n });\n appendRhizohVoiceLiveEventV0(rhizohVoiceLiveSessions.get(socket), \"VOICE_LIVE_SESSION_STARTED\", `${sessionId}:live_session`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_START, {\n ok: true,\n sessionId,\n schema: \"castle.rhizoh.voice_live.gateway_lane.v0\"\n });\n}\n\nfunction handleRhizohVoiceLiveChunkV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const sequence = validateRhizohVoiceLiveChunkPayloadV0(payload, {\n expectedChunkIndex: session.nextChunkIndex\n });\n if (!sequence.ok) {\n rhizohVoiceLiveSessions.delete(socket);\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:chunk_reject`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: sequence.error,\n expectedChunkIndex: sequence.expectedChunkIndex,\n chunkIndex: sequence.chunkIndex\n });\n return;\n }\n const decoded = decodeRhizohVoiceLiveChunkV0(payload);\n if (!decoded.ok) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: decoded.error\n });\n return;\n }\n if (\n session.chunks.length >= RHIZOH_VOICE_LIVE_MAX_CHUNKS ||\n session.totalBytes + decoded.buffer.length > RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES\n ) {\n rhizohVoiceLiveSessions.delete(socket);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_audio_too_large\"\n });\n return;\n }\n session.chunks.push(decoded.buffer);\n session.chunkIndexes.push(sequence.chunkIndex);\n session.totalBytes += decoded.buffer.length;\n session.nextChunkIndex += 1;\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_CHUNK_ACCEPTED\", `${session.sessionId}:chunk:${sequence.chunkIndex}`);\n}\n\nfunction handleRhizohVoiceLiveStopV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n rhizohVoiceLiveSessions.delete(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const lastChunkIndex = Number(payload?.lastChunkIndex);\n if (Number.isInteger(lastChunkIndex) && lastChunkIndex !== session.chunks.length) {\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_SEQUENCE_REJECTED\", `${session.sessionId}:stop_mismatch`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_chunk_count_mismatch\",\n expectedChunkCount: lastChunkIndex,\n receivedChunkCount: session.chunks.length\n });\n return;\n }\n appendRhizohVoiceLiveEventV0(session, \"VOICE_LIVE_STOP_RECEIVED\", `${session.sessionId}:stop:${session.chunks.length}`);\n const audio = Buffer.concat(session.chunks);\n if (!audio.length) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"audio_empty\"\n });\n return;\n }\n runRhizohVoiceTranscribeV3({\n path: String(payload?.path || session.path || \"both\"),\n audioBase64: audio.toString(\"base64\"),\n mimeType: String(payload?.mimeType || session.mimeType || \"audio/webm\"),\n languageCode: String(payload?.languageCode || session.languageCode || \"tr-TR\"),\n traceId: String(payload?.traceId || session.traceId || \"\"),\n sessionId: session.sessionId,\n transport: \"gateway_ws_live\"\n })\n .then((result) => {\n appendRhizohVoiceLiveEventV0(session, result?.ok ? \"VOICE_TRANSCRIBE_FINAL\" : \"VOICE_TRANSCRIBE_FAILED\", `${session.sessionId}:final`);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_FINAL, {\n ...result,\n sessionId: session.sessionId,\n transportPath: \"gateway_ws_live\",\n bytes: audio.length,\n chunkCount: session.chunks.length,\n chunkSequence: Object.freeze({\n ok: true,\n firstChunkIndex: session.chunkIndexes[0] || null,\n lastChunkIndex: session.chunkIndexes[session.chunkIndexes.length - 1] || null,\n receivedChunkCount: session.chunks.length\n }),\n eventTrace: Object.freeze((session.events || []).slice(-16)),\n recordedMs: Math.max(0, Date.now() - session.startedAtMs)\n });\n })\n .catch((e) => {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_transcribe_failed\",\n detail: String(e?.message || e)\n });\n });\n}\n\nfunction removeCastleSocialClientFromAllRooms(clientId) {\n const cid = String(clientId || \"\");\n if (!cid) return;\n for (const room of castleSocialRoomState.values()) {\n room.members.delete(cid);\n }\n}\n\nfunction buildCastleSocialRoomPayload(roomKey) {\n const room = castleSocialRoomState.get(roomKey);\n if (!room) return null;\n const now = Date.now();\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const roster = [...room.members.values()].map((m) => ({\n userId: m.userId,\n lastMs: m.lastMs,\n ...m.sp\n }));\n const peerFeeds =\n room.walPeers && room.walPeers.size\n ? [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }))\n : [];\n return { castleRoomKey: roomKey, seq: room.seq, roster, peerFeeds, ts: now };\n}\n\nconst CASTLE_WAL_PEER_MAX_FEED_BYTES = 48 * 1024;\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {{ ok: boolean, code?: string, reason?: string }} gate\n */\nfunction rejectWalPeerIngressV0(socket, gate) {\n const code = gate.code || \"wal_peer_rejected\";\n recordWalPeerRejectV0(code);\n recordSubstrateOtelEventV0(\"wal_peer_reject\", { code, reason: String(gate.reason || \"\").slice(0, 120) });\n socket.send(\n JSON.stringify(\n createEnvelope(WS_MESSAGE.ERROR, {\n error: gate.reason || \"WAL peer ingress rejected.\",\n code\n })\n )\n );\n}\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {Record<string, unknown> | null | undefined} walPeerFeed\n * @returns {boolean}\n */\nfunction gateWalPeerIngressV0(socket, walPeerFeed) {\n const authGate = assertWalPeerSocketAuthorityV0(socket);\n if (!authGate.ok) {\n rejectWalPeerIngressV0(socket, authGate);\n return false;\n }\n const sigGate = validateWalPeerFeedSignatureV0(\n walPeerFeed && typeof walPeerFeed === \"object\" ? walPeerFeed : {}\n );\n if (!sigGate.ok) {\n rejectWalPeerIngressV0(socket, sigGate);\n return false;\n }\n return true;\n}\n\nfunction handleCastleWalPeerFeed(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_WAL_PEER_MAX_FEED_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_WAL_PEER_FEED too large.\" })));\n return;\n }\n const walPeerFeed = p.walPeerFeed && typeof p.walPeerFeed === \"object\" ? p.walPeerFeed : {};\n if (!gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const castleRoomKey = String(p.castleRoomKey || \"wal:convergence\").slice(0, 64);\n const castleId = String(p.castleId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId,\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: room.members.get(socket.clientId)?.sp ?? {}\n });\n const peerFeeds = [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }));\n const body = {\n castleRoomKey,\n seq: room.seq,\n peerFeeds,\n ts: now\n };\n const env = createEnvelope(WS_MESSAGE.CASTLE_WAL_PEER_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys(), ...room.walPeers.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction handleCastleSocialPulse(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_SOCIAL_MAX_PULSE_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_SOCIAL_PULSE too large.\" })));\n return;\n }\n const castleRoomKey = String(p.castleRoomKey || \"default\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n const sp = p.socialPulse && typeof p.socialPulse === \"object\" ? p.socialPulse : {};\n const walPeerFeed =\n p.walPeerFeed && typeof p.walPeerFeed === \"object\"\n ? p.walPeerFeed\n : sp.walPeerFeed && typeof sp.walPeerFeed === \"object\"\n ? sp.walPeerFeed\n : null;\n if (walPeerFeed && !gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const spNext = walPeerFeed ? { ...sp, walPeerFeed } : { ...sp };\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: spNext\n });\n if (walPeerFeed) {\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId: String(p.castleId || userId).slice(0, 64),\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n }\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const body = buildCastleSocialRoomPayload(castleRoomKey);\n if (!body) return;\n const env = createEnvelope(WS_MESSAGE.CASTLE_SOCIAL_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcast(msgObj) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcastState() {\n broadcast(\n createEnvelope(WS_MESSAGE.BROADCAST_STATE, {\n broadcasterClientId,\n viewers: [...wss.clients].filter((c) => c.readyState === 1).length\n })\n );\n}\n\nfunction currentSpiralStatePayload() {\n return {\n rooms: spiralState.rooms,\n characters: [...spiralState.characters.values()]\n };\n}\n\nfunction broadcastSpiralState() {\n broadcast(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload()));\n}\n\nfunction ensureRoom(roomId, fallbackName = null) {\n if (!roomId) return spiralState.rooms[0].id;\n const exists = spiralState.rooms.find((r) => r.id === roomId);\n if (exists) return exists.id;\n const created = { id: roomId, name: fallbackName || `Spiral-${roomId.slice(-4)}` };\n spiralState.rooms.push(created);\n return created.id;\n}\n\nfunction syncSpiralCharactersWithWorld(snapshot) {\n const agents = snapshot?.agents || [];\n for (const a of agents) {\n if (!spiralState.characters.has(a.id)) {\n spiralState.characters.set(a.id, {\n id: a.id,\n name: a.id,\n role: \"agent\",\n roomId: spiralState.rooms[0].id,\n source: \"world\"\n });\n }\n }\n}\n\nfunction broadcastExcept(msgObj, exceptSocket) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client !== exceptSocket && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction validateInputFrame(payload, socket) {\n const stats = clientStats.get(socket.clientId) || { lastFrame: 0, windowStart: Date.now(), msgCount: 0 };\n const now = Date.now();\n if (now - stats.windowStart > 5000) {\n stats.windowStart = now;\n stats.msgCount = 0;\n }\n stats.msgCount += 1;\n clientStats.set(socket.clientId, stats);\n if (stats.msgCount > 120) return \"Rate limit exceeded.\";\n\n if (!payload || !Number.isInteger(payload.frame)) return \"frame must be integer.\";\n if (payload.frame <= stats.lastFrame) return \"frame must be monotonic.\";\n const commands = Array.isArray(payload.commands) ? payload.commands : [];\n if (commands.length > 8) return \"Too many commands in one frame.\";\n for (const cmd of commands) {\n if (!cmd || typeof cmd.command !== \"string\") return \"Invalid command format.\";\n if (\n cmd.command !== \"SPAWN_AGENT\" &&\n cmd.command !== \"LIST_CASTLES\" &&\n cmd.command !== \"SPAWN_ENTITY\" &&\n cmd.command !== \"CREATE_CASTLE\"\n ) return \"Unsupported command.\";\n }\n stats.lastFrame = payload.frame;\n clientStats.set(socket.clientId, stats);\n return null;\n}\n\nfunction validateSignalPayload(payload) {\n if (!payload || typeof payload !== \"object\") return \"Invalid signaling payload.\";\n const signalType = payload.signalType;\n if (![\"OFFER\", \"ANSWER\", \"ICE\"].includes(signalType)) return \"Invalid signal type.\";\n if (typeof payload.to !== \"string\" || payload.to.length < 3) return \"Invalid signal destination.\";\n if (signalType !== \"ICE\" && JSON.stringify(payload.sdp || {}).length > 16 * 1024) return \"SDP too large.\";\n if (signalType === \"ICE\" && JSON.stringify(payload.candidate || {}).length > 6 * 1024) return \"ICE too large.\";\n return null;\n}\n\nfunction sanitizeRawMessage(raw) {\n if (!raw) return null;\n const text = raw.toString();\n if (Buffer.byteLength(text, \"utf8\") > MAX_MESSAGE_BYTES) return null;\n return text;\n}\n\nwss.on(\"connection\", async (socket, req) => {\n const origin = req?.headers?.origin || \"\";\n if (!httpCors.isWebSocketOriginAllowed(origin)) {\n socket.close(1008, \"Origin not allowed\");\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const path = req?.url || \"\";\n const query = new URL(`ws://localhost${path}`).searchParams;\n const token = query.get(\"token\");\n if (!token || token !== REQUIRED_GATEWAY_TOKEN) {\n socket.close(1008, \"Invalid gateway token\");\n return;\n }\n }\n if (REQUIRE_AUTH) {\n const result = await verifyClientToken(req);\n if (!result.ok) {\n const isLocal = origin.includes(\"localhost\") || origin.includes(\"127.0.0.1\") || !origin;\n if (!(ALLOW_DEV_ANON && isLocal)) {\n socket.close(1008, result.reason);\n return;\n }\n socket.auth = { ok: true, kind: \"dev-anon\", user: { uid: \"dev-anon\" } };\n } else {\n socket.auth = result;\n }\n }\n\n socket.clientId = `c-${Math.random().toString(36).slice(2, 8)}`;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.HELLO, { message: \"Castle Gateway online.\", clientId: socket.clientId })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.WORLD_SNAPSHOT, orchestrator.worldSnapshot())));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.BROADCAST_STATE, { broadcasterClientId, viewers: [...wss.clients].filter((c) => c.readyState === 1).length })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload())));\n broadcastExcept(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }), socket);\n\n socket.on(\"message\", (raw) => {\n const safeRaw = sanitizeRawMessage(raw);\n if (!safeRaw) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Message too large or invalid.\" })));\n return;\n }\n const parsed = safeJsonParse(safeRaw);\n if (!parsed?.type) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid message.\" })));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND_TEXT) {\n const canonical = parseTextToCommand(parsed.payload?.text);\n if (!canonical) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Unknown command text.\" })));\n return;\n }\n const result = orchestrator.applyCommand(canonical);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND) {\n const result = orchestrator.applyCommand(parsed.payload);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n }\n\n if (parsed.type === WS_MESSAGE.INPUT_FRAME) {\n const err = validateInputFrame(parsed.payload, socket);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const frame = parsed.payload.frame;\n const commands = Array.isArray(parsed.payload?.commands) ? parsed.payload.commands : [];\n orchestrator.queueInputFrame(socket.clientId, frame, commands);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, queued: true, frame })));\n }\n\n if (parsed.type === WS_MESSAGE.SIGNAL) {\n const payload = parsed.payload || {};\n const err = validateSignalPayload(payload);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const signalType = payload.signalType;\n const to = payload.to;\n for (const client of wss.clients) {\n if (client.clientId === to && client.readyState === 1) {\n client.send(JSON.stringify(createEnvelope(WS_MESSAGE.SIGNAL, { ...payload, from: socket.clientId })));\n break;\n }\n }\n }\n\n if (parsed.type === WS_MESSAGE.DRONE_TELEMETRY) {\n const payload = parsed.payload || {};\n const json = JSON.stringify(payload);\n if (Buffer.byteLength(json, \"utf8\") > 48 * 1024) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"DRONE_TELEMETRY payload too large.\" })));\n return;\n }\n broadcastExcept(createEnvelope(WS_MESSAGE.DRONE_TELEMETRY, { ...payload, relayFrom: socket.clientId }), socket);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_START) {\n handleRhizohVoiceLiveStartV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_CHUNK) {\n handleRhizohVoiceLiveChunkV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_STOP) {\n handleRhizohVoiceLiveStopV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_SOCIAL_PULSE) {\n handleCastleSocialPulse(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_WAL_PEER_FEED) {\n handleCastleWalPeerFeed(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.OPEN_DATA_QUERY) {\n queryOpenData(parsed.payload)\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: true, result })));\n })\n .catch((e) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: false, error: e.message })));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_QUERY) {\n runRhizohBrainV2({\n ...(parsed.payload || {}),\n uid: socket?.auth?.user?.uid || \"unknown\"\n })\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, result)));\n })\n .catch(async () => {\n const fallback = await runRhizohBrain(parsed.payload || {}).catch(() => ({\n ok: false,\n reply: \"Rhizoh temporary disturbance. Habitat remains stable.\"\n }));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, fallback)));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.BROADCAST_REGISTER) {\n const role = String(parsed.payload?.role || \"\");\n if (role === \"GENESIS_BROADCAST_AGENT\") {\n broadcasterClientId = socket.clientId;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, registered: role })));\n broadcastState();\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.STUDIO_CUE) {\n // Only registered broadcaster can push global cues.\n if (!broadcasterClientId || socket.clientId !== broadcasterClientId) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Only broadcaster agent can emit STUDIO_CUE.\" })));\n return;\n }\n const cue = parsed.payload || {};\n if (typeof cue !== \"object\") {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid STUDIO_CUE payload.\" })));\n return;\n }\n broadcast(createEnvelope(WS_MESSAGE.STUDIO_CUE, cue));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_JOIN_ROOM) {\n const requested = String(parsed.payload?.roomId || \"\").trim();\n const requestedName = String(parsed.payload?.roomName || \"\").trim();\n const roomId = ensureRoom(requested || `room-${Math.random().toString(36).slice(2, 7)}`, requestedName || null);\n spiralState.activeByClient.set(socket.clientId, roomId);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, roomId })));\n broadcastSpiralState();\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_CREATE_CHARACTER) {\n const name = String(parsed.payload?.name || \"\").trim().slice(0, 32) || `AI-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;\n const role = String(parsed.payload?.role || \"agent\").trim().slice(0, 24) || \"agent\";\n const roomId = ensureRoom(\n String(parsed.payload?.roomId || spiralState.activeByClient.get(socket.clientId) || spiralState.rooms[0].id),\n null\n );\n\n // Character creation always maps to an authoritative world agent.\n const result = orchestrator.applyCommand({\n command: COMMAND.SPAWN_AGENT,\n params: { targetCastleId: \"castle-01\" }\n });\n if (!result?.ok || !result?.data?.id) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Character creation failed.\" })));\n return;\n }\n const id = result.data.id;\n spiralState.characters.set(id, { id, name, role, roomId, source: \"spawned\" });\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, characterId: id, roomId })));\n broadcastSpiralState();\n return;\n }\n });\n\n socket.on(\"close\", () => {\n if (socket.clientId === broadcasterClientId) broadcasterClientId = null;\n clientStats.delete(socket.clientId);\n rhizohVoiceLiveSessions.delete(socket);\n spiralState.activeByClient.delete(socket.clientId);\n removeCastleSocialClientFromAllRooms(socket.clientId);\n broadcast(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }));\n broadcastState();\n });\n});\n\nsetInterval(() => {\n void (async () => {\n try {\n const { frame, snapshot, delta, packedDelta } = orchestrator.tick(1 / 20);\n syncSpiralCharactersWithWorld(snapshot);\n broadcast(createEnvelope(WS_MESSAGE.NETWORK_TICK, { frame }));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_TICK, snapshot));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA, delta));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA_PACKED, packedDelta));\n broadcastSpiralState();\n await runAcademyEventTick();\n const executed = await tickAutomations(Date.now());\n for (const a of executed) {\n await addTranscript(a.uid, {\n source: \"planner\",\n eventType: \"automation-executed\",\n text: `Automation executed: ${a.title} (${a.action})`,\n roomId: \"studio-main\",\n meta: { automationId: a.id, action: a.action, deviceId: a.deviceId || \"\" }\n });\n }\n } catch (e) {\n console.error(\"[GATEWAY] world tick / automations loop error:\", String(e?.message || e));\n }\n })();\n}, 50);\n\n(async () => {\n await rhizohRuntime.telemetry.initOpenTelemetry();\n if (String(process.env.CASTLE_GENESIS_DISK_PERSIST ?? \"\").trim() === \"\") {\n process.env.CASTLE_GENESIS_DISK_PERSIST = \"1\";\n }\n const genesisDiskBoot = await hydrateGenesisContinuityPersistenceBootV0();\n if (!genesisDiskBoot.ok) {\n console.warn(\"[GATEWAY] genesis disk hydrate failed; continuing with in-memory genesis continuity\");\n }\n httpServer.listen(PORT, () => {\n startGenesisCanonicalClock();\n installGenesisCheckpointSurfaceGetter(buildGenesisRuntimeSurfacePayloadLive);\n setGenesisContinuityAfterPublishHook((seq) => {\n noteGenesisCheckpointSeqCommitted(seq);\n noteGenesisContinuitySeqAcceptedV0(seq);\n });\n startGenesisContinuityInfraSampler(buildGenesisRuntimeSurfacePayloadLive, 2000);\n logProductionObservatorySurfaceGuardsV0();\n logGatewaySubstrateAuthorityGuardsV0();\n console.log(`[GATEWAY] ws/http://localhost:${PORT}`);\n console.log(\n `[GATEWAY] genesis continuity: GET ${rhizohRuntime.routes.genesisRuntime} | POST ${rhizohRuntime.routes.genesisIngress} | SSE ${rhizohRuntime.routes.genesisStream} | replay ${rhizohRuntime.routes.genesisReplay} | diff ${rhizohRuntime.routes.genesisReplayDiff} | equiv ${rhizohRuntime.routes.genesisReplayEquivalence} | analytics ${rhizohRuntime.routes.genesisReplayAnalytics} | evolution ${rhizohRuntime.routes.genesisReplayEvolution}`\n );\n if (String(process.env.CASTLE_GENESIS_OBSERVATORY_LIVE_V01 ?? \"\").trim() === \"1\") {\n console.log(\"GENESIS OBSERVATORY LIVE v0.1 — READ ONLY PHASE STARTED\");\n }\n });\n})().catch((e) => {\n console.error(\"[GATEWAY] fatal boot (before or during listen):\", e);\n process.exit(1);\n});\n"}, "ec88c128d8f7b6525a5f96819fb9fc9cbe0970c3": {"t": 1785754154.91346, "n": 161493, "h": "4b3f5445e991b3c5", "c": "import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport dotenv from \"dotenv\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconsole.log(\"🔥 [GENESIS_BOOT] entry file:\", __filename);\nconsole.log(\"🔥 [GENESIS_BOOT] node env:\", process.env.NODE_ENV);\nconsole.log(\"🔥 [GENESIS_BOOT] pid:\", process.pid);\nconsole.log(\"🧬 [GENESIS_BOOT] cwd:\", process.cwd());\nconsole.log(\"🧬 [GENESIS_BOOT] import.meta.url:\", import.meta.url);\ndotenv.config({ path: path.join(__dirname, \"..\", \".env\") });\ndotenv.config({ path: path.join(__dirname, \"..\", \".env.local\"), override: true });\n\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport { createServer } from \"node:http\";\nimport { createOrchestrator } from \"@castle/orchestrator\";\nimport { parseTextToCommand } from \"@castle/command-dsl\";\nimport { WS_MESSAGE, COMMAND, createEnvelope, safeJsonParse } from \"@castle/protocol\";\nimport { queryOpenData } from \"./openData.js\";\nimport { verifyClientToken } from \"./auth.js\";\nimport { runRhizohBrain } from \"./rhizohBrain.js\";\nimport { queryRhizohLlm } from \"./rhizohLlmGateway.js\";\nimport { rhizohGatewayTurn } from \"./rhizohGatewayTurn.js\";\nimport {\n RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n runRhizohVoiceTranscribeV3,\n rhizohVoiceTranscribeEnvV3\n} from \"./rhizohVoiceTranscribeV3.js\";\nimport { createHttpCorsPolicy, normalizeHttpOrigin, parseAllowedOriginsFromEnv } from \"./httpCorsPolicyV1.js\";\nimport {\n applyLanguagePropagationToLlmPayloadV1,\n applyLanguagePropagationToVoiceBodyV1,\n buildRhizohLanguagePropagationEchoV1,\n parseRhizohLanguagePropagationV1\n} from \"./rhizohLanguagePropagationV1.js\";\nimport { trimRhizohLlmGatewayPayloadV1 } from \"./rhizohLlmPayloadTrimV1.js\";\nimport {\n meshAppendDelta,\n meshContinuityAggregate,\n meshJoin,\n meshLeave,\n meshReplayFromNodeId,\n meshReplayFromSeq,\n meshSnapshot,\n meshSubscribeSse\n} from \"./presenceRoomMesh.js\";\nimport { runRhizohBrainV2 } from \"./rhizohBrainV2.js\";\nimport {\n listConnections,\n createConnection,\n updateConnection,\n deleteConnection,\n setDefaultConnection,\n resolveConnection\n} from \"./llmConnectionsStore.js\";\nimport { checkHttpRateLimit, getHttpClientIp } from \"./castleHttpRateLimit.js\";\nimport {\n submitAbuseReportV0,\n listModerationQueueV0,\n softBlockUserV0,\n removeSoftBlockV0,\n listSoftBlockedUsersV0\n} from \"./ops/moderationMvpV0.js\";\nimport { listRecentAgentSnapshotsV0 } from \"./ops/agentObservabilityV0.js\";\nimport { readAgentContainmentConfigV0 } from \"./ops/agentContainmentV0.js\";\nimport { readCostContainmentConfigV0, getCostContainmentStatsV0 } from \"./ops/costContainmentV0.js\";\nimport { getGlobalCostLedgerSnapshotV0, getGclBackendStatusV0 } from \"./ops/globalCostLedgerV0.js\";\nimport { evaluateEconomicStrategyL3V0 } from \"./ops/economicStrategyEngineL3V0.js\";\nimport { getPhasedRolloutStatsV0, readPhasedRolloutTierV0 } from \"./ops/phasedRolloutV0.js\";\nimport { buildUnifiedStateNarrativeV0 } from \"./ops/unifiedStateNarrativeV0.js\";\nimport { resolveHardeningTenantIdV0 } from \"./ops/narrativeTenantIsolationV0.js\";\nimport {\n classifyStressResponseV0,\n verifyStressTaxonomyCoverageV0,\n verifyStressConflictResolutionV0\n} from \"./ops/stressResponseTaxonomyV0.js\";\nimport { verifyResolutionStabilityEnvelopeV0 } from \"./ops/resolutionStabilityEnvelopeV0.js\";\nimport { logLlmAccess } from \"./castleLlmAudit.js\";\nimport { appendMemory, listMemories, getMemoryContext, getPersonaGoalMemory, setPersonaGoalMemory, autoCompactMemories } from \"./memoryStore.js\";\nimport { getFirebasePersistence } from \"./firebasePersistence.js\";\nimport { registerAgentIdentity, listAgentIdentities, getAgentIdentity, updateAgentIdentity } from \"./agentIdentityStore.js\";\nimport { queueAcademyEvent, listAcademyEvents, resolveAcademyEvent, runAcademyEventTick } from \"./academyEventEngine.js\";\nimport { enqueuePdfJob, getPdfJob } from \"./pdfPipeline.js\";\nimport {\n createPublishSession,\n listPublishSessions,\n updatePublishSession,\n addTranscript,\n listTranscripts,\n searchMetadata,\n registerDevice,\n listDevices,\n updateDevice,\n appendTelemetry,\n listTelemetry,\n createAutomation,\n listAutomations,\n tickAutomations,\n enqueueCommand,\n listCommands,\n updateCommandStatus,\n registerSocialChannel,\n listSocialChannels\n} from \"./studioOpsStore.js\";\nimport { metrics as infraMetrics } from \"./infra/metrics.js\";\nimport { renderRhizohPrometheusMetrics, rhizohEnterpriseMetrics } from \"./infra/rhizohEnterpriseMetrics.js\";\nimport { scoreHealth } from \"./infra/healthScore.js\";\nimport { getTraceById, listTracesBySession } from \"./infra/traceRegistry.js\";\nimport { canonicalEpistemicSealString, hashAndSignEpistemicSeal, EPISTEMIC_SEAL_SCHEMA } from \"./epistemicSeal.js\";\nimport { persistEpistemicLedgerBatch } from \"./epistemicLedgerStore.js\";\nimport { persistEpistemicForecastBatch } from \"./epistemicForecastStore.js\";\nimport { buildRhizohExternalGroundTruthPayload } from \"./rhizohExternalGroundTruthGateway.js\";\nimport { ingestRhizohExternalLossBatchHttp } from \"./rhizohExternalLossIngestGateway.js\";\nimport {\n ingestRhizohProductOutcomeHttp,\n listRhizohProductOutcomeAggregates,\n verifyRhizohOutcomeHmac,\n verifyRhizohOutcomeSourceToken\n} from \"./rhizohProductOutcomeIngestGateway.js\";\nimport { initRhizoh } from \"./rhizohProductionBootstrap.js\";\nimport { handleAcademicObservatoryExportGetV0 } from \"./rhizoh/academicObservatoryHttpV0.js\";\nimport {\n handleLiveSportsBundleGetV0,\n handleLiveNewsHeadlinesGetV0,\n handleLiveWorldFeedGetV0\n} from \"./rhizoh/liveWorldFeedHttpV0.js\";\nimport {\n buildGenesisRuntimeSurfacePayload,\n recordGenesisEpistemicLedgerPersisted,\n recordGenesisEpistemicSealIssued,\n startGenesisCanonicalClock\n} from \"./genesisRuntimeSurfaceV0.js\";\nimport { registerGenesisSseClient, getGenesisContinuitySeq, setGenesisContinuityAfterPublishHook } from \"./genesisContinuityStreamHubV0.js\";\nimport { computeGenesisReplayFingerprintV0 } from \"./genesisReplayFingerprintV0.js\";\nimport { buildGenesisGatewayCapabilitiesV0 } from \"./genesisGatewayCapabilitiesV0.js\";\nimport {\n installGenesisCheckpointSurfaceGetter,\n noteGenesisCheckpointSeqCommitted,\n getLatestGenesisSignedCheckpoint\n} from \"./genesisContinuityCheckpointV0.js\";\nimport { hydrateGenesisContinuityPersistenceBootV0 } from \"./genesisContinuityHydrateBootV0.js\";\nimport { startGenesisContinuityInfraSampler } from \"./genesisContinuityInfraSamplerV0.js\";\nimport {\n genesisCheckpointQueryBySeqV0,\n genesisCheckpointQueryRangeV0,\n genesisCheckpointQueryLineageV0\n} from \"./genesisCheckpointQueryV0.js\";\nimport {\n genesisContinuityEventArchiveEnabled,\n queryGenesisContinuityEventArchiveV0,\n GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION\n} from \"./genesisContinuityEventArchiveV0.js\";\nimport { resolveGenesisReplayRouterV1, GENESIS_REPLAY_ROUTER_SCHEMA } from \"./genesisReplayRouterV1.js\";\nimport {\n computeGenesisReplayTemporalDiffV1,\n GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA\n} from \"./genesisReplayTemporalDiffV1.js\";\nimport { compareGenesisReplayEquivalenceV1 } from \"./genesisReplayEquivalenceV1.js\";\nimport { computeGenesisReplayAnalyticsV1 } from \"./genesisReplayAnalyticsV1.js\";\nimport { computeGenesisReplayEvolutionV1 } from \"./genesisReplayEvolutionV1.js\";\nimport { handleStripeCheckoutCreate, handleStripeMembershipWebhook } from \"./stripeWebhookMembership.js\";\nimport { buildRealityHealthPayload } from \"./edgeRealityConsistencyV1.js\";\nimport {\n assertWalPeerSocketAuthorityV0,\n buildGatewaySubstrateAuthoritySnapshotV0,\n logGatewaySubstrateAuthorityGuardsV0,\n validateWalPeerFeedSignatureV0\n} from \"./gatewaySubstrateAuthorityV0.js\";\nimport {\n buildSubstrateOperationalSnapshotV0,\n recordWalPeerRejectV0,\n renderSubstratePrometheusMetricsV0\n} from \"./infra/substrateOperationalMetrics.js\";\nimport { ingestClientSubstrateHealthV0 } from \"./rhizohSubstrateHealthIngestV0.js\";\nimport { recordSubstrateOtelEventV0 } from \"./infra/opentelemetrySubstrateV0.js\";\nimport {\n GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n getGenesisWorldObservationIngressStatusV0,\n ingestGenesisWorldObservationV0\n} from \"./genesisContinuityClientIngressV0.js\";\nimport {\n getGenesisContinuitySeqAuditV0,\n noteGenesisContinuitySeqAcceptedV0\n} from \"./genesisContinuitySeqGapV0.js\";\n\n/** Render / Fly / Railway: `PORT` — yoksa `CASTLE_GATEWAY_PORT` — yoksa 8090. */\nconst PORT =\n Number(process.env.PORT) ||\n Number(process.env.CASTLE_GATEWAY_PORT) ||\n 8090;\n/** Spiral / world WebSocket server — set after `wss` init; used by genesis runtime surface. */\nlet spiralWssForGenesis = null;\nconst MAX_MESSAGE_BYTES = Number(process.env.CASTLE_MAX_MESSAGE_BYTES || 32 * 1024);\nconst REQUIRED_GATEWAY_TOKEN = process.env.CASTLE_GATEWAY_TOKEN || \"\";\nconst ALLOWED_ORIGINS = parseAllowedOriginsFromEnv().fromList;\nconst httpCors = createHttpCorsPolicy();\n\n/** @returns {string|null} `Access-Control-Allow-Origin`; null = browser CORS fail. */\nfunction accessControlAllowOriginValue(req) {\n return httpCors.accessControlAllowOriginValue(req);\n}\n\nfunction applyHttpCorsHeaders(req, res) {\n return httpCors.applyHttpCorsHeaders(req, res);\n}\nconst REQUIRE_AUTH = process.env.CASTLE_REQUIRE_AUTH === \"true\";\nconst ALLOW_DEV_ANON = process.env.CASTLE_ALLOW_DEV_ANON !== \"false\";\nconst ALLOW_DEV_HTTP_UID = process.env.CASTLE_ALLOW_DEV_HTTP_UID !== \"false\";\n/** Presence mesh: allow `X-Castle-Guest-Id` when Firebase auth absent (default on). Set `false` to require auth only. */\nconst ALLOW_MESH_GUEST = process.env.CASTLE_ALLOW_MESH_GUEST !== \"false\";\nconst SOCIAL_RETRY_MAX = Math.max(0, Math.min(5, Number(process.env.CASTLE_SOCIAL_RETRY_MAX || 2)));\nconst SOCIAL_RETRY_BASE_MS = Math.max(100, Number(process.env.CASTLE_SOCIAL_RETRY_BASE_MS || 700));\nconst TELEGRAM_BOT_TOKEN = process.env.CASTLE_TELEGRAM_BOT_TOKEN || \"\";\nconst WHATSAPP_TOKEN = process.env.CASTLE_WHATSAPP_TOKEN || \"\";\nconst WHATSAPP_PHONE_NUMBER_ID = process.env.CASTLE_WHATSAPP_PHONE_NUMBER_ID || \"\";\nconst RL_RHIZOH_LLM_PER_MIN = Math.max(5, Number(process.env.CASTLE_RL_RHIZOH_LLM_PER_MIN || 40));\nconst RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN || 30)\n);\nconst RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN || 120));\nconst RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN = Math.max(\n 10,\n Number(process.env.CASTLE_RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN || 60)\n);\nconst RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN = Math.max(8, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN || 48));\nconst RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN = Math.max(4, Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN || 24));\nconst RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN || 18)\n);\nconst RL_EPISTEMIC_SEAL_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_EPISTEMIC_SEAL_PER_MIN || 120));\nconst RL_EPISTEMIC_LOG_PER_MIN = Math.max(20, Number(process.env.CASTLE_RL_EPISTEMIC_LOG_PER_MIN || 240));\nconst EPISTEMIC_SEAL_SECRET = String(\n process.env.CASTLE_EPISTEMIC_SEAL_SECRET || process.env.CASTLE_GATEWAY_TOKEN || \"\"\n).trim();\nconst RL_LLM_CONN_TEST_PER_MIN = Math.max(2, Number(process.env.CASTLE_RL_LLM_CONNECTION_TEST_PER_MIN || 8));\n\n/** One-shot production warnings: CORS surface, auth toggle, dev bypass flags (observatory deploy hygiene). */\nfunction logProductionObservatorySurfaceGuardsV0() {\n if (String(process.env.NODE_ENV || \"\").toLowerCase() !== \"production\") return;\n const tag = \"[GATEWAY_PROD_GUARD]\";\n const primaryCors = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n const corsListEmpty = ALLOWED_ORIGINS.length === 0;\n if (corsListEmpty && !primaryCors) {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty and CASTLE_HTTP_CORS_ORIGIN empty — production still merges RHIZOH_BROWSER_ORIGINS_V1 defaults; set env for preview/staging hosts`\n );\n } else if (corsListEmpty && primaryCors && primaryCors !== \"*\") {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty — only CASTLE_HTTP_CORS_ORIGIN set; add web.app + firebaseapp.com (and previews) to CASTLE_ALLOWED_ORIGINS if clients use alternate hostnames`\n );\n }\n if (primaryCors && /localhost|127\\.0\\.0\\.1/i.test(primaryCors)) {\n console.warn(`${tag} CORS: CASTLE_HTTP_CORS_ORIGIN is localhost while NODE_ENV=production`);\n }\n if (REQUIRE_AUTH) {\n console.warn(\n `${tag} AUTH: CASTLE_REQUIRE_AUTH=true — clients must send a valid gateway credential (otherwise silent 401 on replay/SSE); EventSource cannot set custom headers unless proxied`\n );\n if (REQUIRED_GATEWAY_TOKEN.length < 16) {\n console.warn(`${tag} AUTH: CASTLE_GATEWAY_TOKEN missing or short while require-auth enabled`);\n }\n }\n if (ALLOW_DEV_ANON) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_ANON is not \"false\" — dev anonymous path may be live in production (set CASTLE_ALLOW_DEV_ANON=false)`\n );\n }\n if (ALLOW_DEV_HTTP_UID) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_HTTP_UID is not \"false\" — X-Castle-Dev-Uid bypass risk (set CASTLE_ALLOW_DEV_HTTP_UID=false)`\n );\n }\n}\n\nconst rhizohRuntime = initRhizoh();\n\n/** İstemci: llmKeySource veya keySource — env | user_connection | auto */\nfunction normalizeClientLlmKeySource(raw) {\n const s = String(raw ?? \"\").trim().toLowerCase();\n if (s === \"env\" || s === \"server\") return \"env\";\n if (s === \"user_connection\" || s === \"connection\" || s === \"user\") return \"user_connection\";\n if (s === \"auto\" || s === \"\") return \"auto\";\n return null;\n}\n\nfunction sendJson(res, statusCode, payload) {\n res.writeHead(statusCode, { \"Content-Type\": \"application/json; charset=utf-8\" });\n res.end(JSON.stringify(payload));\n}\n\nfunction logRhizohHealth(stage, detail = {}) {\n try {\n console.info(`[CASTLE_${String(stage || \"unknown\")}]`, detail && typeof detail === \"object\" ? detail : {});\n } catch {\n /* noop */\n }\n}\n\nfunction renderInfraMetricsProm() {\n const lines = [\n `castle_gateway_events_processed_total ${infraMetrics.eventsProcessed}`,\n `castle_gateway_duplicate_reject_total ${infraMetrics.duplicateRejects}`,\n `castle_gateway_enqueue_latency_ms ${infraMetrics.enqueueLatencyMs}`,\n `castle_gateway_queue_depth ${infraMetrics.queueDepth}`,\n `castle_gateway_queue_lag_seconds ${(Number(infraMetrics.queueLag || 0) / 1000).toFixed(3)}`,\n `castle_gateway_errors_total ${infraMetrics.errors}`\n ];\n const buckets = infraMetrics.enqueueLatencyBuckets || {};\n for (const k of Object.keys(buckets)) {\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"${k}\"} ${buckets[k]}`);\n }\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"+Inf\"} ${infraMetrics.eventsProcessed}`);\n if (process.env.CASTLE_SUBSTRATE_METRICS !== \"0\") {\n lines.push(renderSubstratePrometheusMetricsV0().trimEnd());\n }\n return lines.join(\"\\n\");\n}\n\nasync function fetchJsonWithTimeout(url, timeoutMs = 1200) {\n const ctl = new AbortController();\n const tid = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const res = await fetch(url, { signal: ctl.signal });\n if (!res.ok) return null;\n return await res.json();\n } catch {\n return null;\n } finally {\n clearTimeout(tid);\n }\n}\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction getHttpPathname(req) {\n let p = String(req.url || \"\").split(\"?\")[0];\n /* Reverse proxies / clients sometimes append `/` — avoid silent 404 on e.g. `/rhizoh/genesis/stream/`. */\n while (p.length > 1 && p.endsWith(\"/\")) {\n p = p.slice(0, -1);\n }\n return p;\n}\n\nfunction rhizohLlmEnvConfigured() {\n const p = String(process.env.CASTLE_LLM_PROVIDER || \"openai\").toLowerCase();\n const key =\n p === \"anthropic\"\n ? process.env.ANTHROPIC_API_KEY\n : p === \"gemini\"\n ? process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY\n : p === \"xai\"\n ? process.env.XAI_API_KEY\n : p === \"deepseek\"\n ? process.env.DEEPSEEK_API_KEY\n : p === \"mistral\"\n ? process.env.MISTRAL_API_KEY\n : p === \"openrouter\"\n ? process.env.OPENROUTER_API_KEY\n : process.env.OPENAI_API_KEY;\n return !!String(key || \"\").trim();\n}\n\n/**\n * @param {import(\"firebase-admin/firestore\").Firestore | null} db\n * @param {number} timeoutMs\n */\nasync function probeFirestoreReachable(db, timeoutMs = 2000) {\n if (!db) return { ok: false, reason: \"no_db\" };\n const timeout = sleep(timeoutMs).then(() => ({ ok: false, reason: \"timeout\" }));\n const work = db\n .collection(\"_castle_gateway_health\")\n .doc(\"ping\")\n .get()\n .then(() => ({ ok: true, reason: \"reachable\" }))\n .catch((e) => ({ ok: false, reason: String(e?.message || e || \"firestore_error\") }));\n return Promise.race([work, timeout]);\n}\n\nfunction stripHtml(text = \"\") {\n return String(text)\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \" \")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \" \")\n .replace(/<[^>]+>/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction haversineMeters(aLat, aLon, bLat, bLon) {\n const R = 6371000;\n const dLat = ((bLat - aLat) * Math.PI) / 180;\n const dLon = ((bLon - aLon) * Math.PI) / 180;\n const sa = Math.sin(dLat / 2);\n const sb = Math.sin(dLon / 2);\n const x = sa * sa + Math.cos((aLat * Math.PI) / 180) * Math.cos((bLat * Math.PI) / 180) * sb * sb;\n return 2 * R * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));\n}\n\nasync function checkPublishTargetHealth(protocol, target) {\n const p = String(protocol || \"\").toUpperCase();\n const t = String(target || \"\").trim();\n if (!t) return { ok: false, reason: \"target_missing\", shouldReconnect: false };\n if (p === \"RTMP\") {\n const ok = t.startsWith(\"rtmp://\") || t.startsWith(\"rtmps://\");\n return { ok, reason: ok ? \"rtmp_target_format_ok\" : \"invalid_rtmp_target\", shouldReconnect: !ok, reconnectInSec: 15 };\n }\n if (p === \"WHIP\" || p === \"SFU\") {\n try {\n const res = await fetch(t, { method: \"OPTIONS\" });\n const ok = res.status < 500;\n return { ok, reason: `http_${res.status}`, shouldReconnect: !ok, reconnectInSec: 8 };\n } catch {\n return { ok: false, reason: \"network_unreachable\", shouldReconnect: true, reconnectInSec: 8 };\n }\n }\n return { ok: true, reason: \"unchecked_protocol\", shouldReconnect: false };\n}\n\nasync function fetchTextFromUrl(url) {\n const res = await fetch(url, { method: \"GET\" });\n if (!res.ok) throw new Error(`fetch_${res.status}`);\n const ct = String(res.headers.get(\"content-type\") || \"\");\n if (ct.includes(\"application/pdf\")) throw new Error(\"pdf_binary_not_supported_directly\");\n const body = await res.text();\n return stripHtml(body).slice(0, 14000);\n}\n\nfunction buildTaskActionGraph(task = \"\", context = {}) {\n const t = String(task || \"\").toLowerCase();\n if (t.includes(\"dinner\") || t.includes(\"akşam yemeği\") || t.includes(\"aksam yemegi\")) {\n return {\n taskTitle: \"Dinner Prep Planner\",\n nodes: [\n { id: \"n1\", title: \"Kitchen preheat\", action: \"smart-oven.preheat\", params: { temperatureC: 190, mode: \"fan\" } },\n { id: \"n2\", title: \"Lighting comfort\", action: \"smart-light.set_scene\", params: { scene: \"warm_dinner\" } },\n { id: \"n3\", title: \"Fridge inventory check\", action: \"smart-fridge.inventory_scan\", params: {} },\n { id: \"n4\", title: \"Reminder notification\", action: \"smart-watch.notify\", params: { text: \"Dinner prep started.\" } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" },\n { from: \"n3\", to: \"n4\" }\n ]\n };\n }\n return {\n taskTitle: task || \"General Planner\",\n nodes: [\n { id: \"n1\", title: \"Safety check\", action: \"device.safety_check\", params: { context } },\n { id: \"n2\", title: \"Primary action\", action: \"device.execute_task\", params: { task } },\n { id: \"n3\", title: \"Report\", action: \"studio.timeline_note\", params: { task } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" }\n ]\n };\n}\n\nfunction buildEthicsPrograms() {\n return {\n policyVersion: \"v1\",\n tracks: [\n {\n id: \"elderly-care-assist\",\n audience: \"elderly\",\n title: \"Elderly Assistance Track\",\n principles: [\"human-in-the-loop\", \"consent-first\", \"low-friction alerts\"],\n modules: [\"medication-reminder\", \"fall-risk-check\", \"family-notify\", \"daily-companion-brief\"]\n },\n {\n id: \"kids-safe-learning\",\n audience: \"children\",\n title: \"Kids Safe Learning Track\",\n principles: [\"guardian-consent\", \"age-appropriate-language\", \"content-filtering\"],\n modules: [\"study-coach\", \"screen-time-balance\", \"safe-social-share\", \"emergency-contact\"]\n }\n ]\n };\n}\n\nasync function deliverViaTelegram(channel, text) {\n const chatId = String(channel.endpoint || \"\").trim();\n if (!TELEGRAM_BOT_TOKEN) throw new Error(\"telegram_token_missing\");\n if (!chatId) throw new Error(\"telegram_chat_id_missing\");\n const url = `https://api.telegram.org/bot${encodeURIComponent(TELEGRAM_BOT_TOKEN)}/sendMessage`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ chat_id: chatId, text })\n });\n if (!res.ok) throw new Error(`telegram_http_${res.status}`);\n return { provider: \"telegram\", status: res.status };\n}\n\nasync function deliverViaWhatsapp(channel, text) {\n const to = String(channel.endpoint || \"\").trim();\n if (!WHATSAPP_TOKEN) throw new Error(\"whatsapp_token_missing\");\n if (!WHATSAPP_PHONE_NUMBER_ID) throw new Error(\"whatsapp_phone_number_id_missing\");\n if (!to) throw new Error(\"whatsapp_to_missing\");\n const url = `https://graph.facebook.com/v22.0/${encodeURIComponent(WHATSAPP_PHONE_NUMBER_ID)}/messages`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${WHATSAPP_TOKEN}` },\n body: JSON.stringify({ messaging_product: \"whatsapp\", to, type: \"text\", text: { body: text } })\n });\n if (!res.ok) throw new Error(`whatsapp_http_${res.status}`);\n return { provider: \"whatsapp\", status: res.status };\n}\n\nasync function deliverViaWebhook(channel, text) {\n const endpoint = String(channel.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"webhook_endpoint_missing\");\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text, platform: channel.platform || \"webhook\", ts: Date.now() })\n });\n if (!res.ok) throw new Error(`webhook_http_${res.status}`);\n return { provider: \"webhook\", status: res.status };\n}\n\nasync function deliverSocialMessage(channel, text) {\n const platform = String(channel.platform || \"webhook\").toLowerCase();\n if (platform === \"telegram\") return deliverViaTelegram(channel, text);\n if (platform === \"whatsapp\") return deliverViaWhatsapp(channel, text);\n return deliverViaWebhook(channel, text);\n}\n\nasync function deliverSocialWithRetry(channel, text) {\n let attempt = 0;\n let lastError = null;\n while (attempt <= SOCIAL_RETRY_MAX) {\n try {\n const result = await deliverSocialMessage(channel, text);\n return { ok: true, attempt: attempt + 1, ...result };\n } catch (error) {\n lastError = error;\n if (attempt >= SOCIAL_RETRY_MAX) break;\n const jitter = Math.floor(Math.random() * 90);\n await sleep(SOCIAL_RETRY_BASE_MS * Math.pow(2, attempt) + jitter);\n }\n attempt += 1;\n }\n return { ok: false, attempt: attempt + 1, error: String(lastError?.message || \"delivery_failed\") };\n}\n\nasync function dispatchCommandToAdapter(device, command) {\n const adapter = String(command?.adapter || device?.adapter || \"websocket\").toLowerCase();\n const endpoint = String(device?.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"device_endpoint_missing\");\n if (adapter === \"mqtt\") {\n // MQTT delivery is proxied via a bridge endpoint (HTTP webhook style).\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n type: \"mqtt-command\",\n topic: `castle/devices/${device.id}/commands`,\n action: command.action,\n params: command.params || {}\n })\n });\n if (!res.ok) throw new Error(`mqtt_bridge_http_${res.status}`);\n return { ok: true, adapter, via: \"http-bridge\", status: res.status };\n }\n if (!endpoint.startsWith(\"ws://\") && !endpoint.startsWith(\"wss://\")) throw new Error(\"invalid_websocket_endpoint\");\n const sendResult = await new Promise((resolve, reject) => {\n const ws = new WebSocket(endpoint);\n const timer = setTimeout(() => {\n try {\n ws.close();\n } catch {\n /* noop */\n }\n reject(new Error(\"websocket_timeout\"));\n }, 3000);\n ws.on(\"open\", () => {\n ws.send(\n JSON.stringify({\n type: \"castle-command\",\n commandId: command.id,\n deviceId: command.deviceId,\n action: command.action,\n params: command.params || {}\n }),\n (err) => {\n clearTimeout(timer);\n ws.close();\n if (err) reject(new Error(\"websocket_send_failed\"));\n else resolve({ ok: true, adapter: \"websocket\", via: \"ws\" });\n }\n );\n });\n ws.on(\"error\", () => {\n clearTimeout(timer);\n reject(new Error(\"websocket_connect_failed\"));\n });\n });\n return sendResult;\n}\n\nfunction readHttpJson(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => {\n if (!buf) return resolve({});\n const payload = safeJsonParse(buf);\n if (!payload || typeof payload !== \"object\") return reject(new Error(\"invalid_json_payload\"));\n resolve(payload);\n });\n req.on(\"error\", reject);\n });\n}\n\n/** Ham gövde (HMAC doğrulaması için). */\nfunction readHttpBodyText(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => resolve(buf));\n req.on(\"error\", reject);\n });\n}\n\nfunction maskOpaqueId(value) {\n const t = String(value || \"\");\n if (!t) return \"(empty)\";\n if (t.length <= 8) return \"****\";\n return `${t.slice(0, 4)}…${t.slice(-2)}`;\n}\n\nfunction readGatewayHttpToken(req) {\n const x = String(req.headers?.[\"x-castle-gateway-token\"] || \"\").trim();\n if (x) return x;\n const auth = String(req.headers?.authorization || \"\");\n const m = /^Bearer\\s+(.+)$/i.exec(auth);\n return m ? String(m[1] || \"\").trim() : \"\";\n}\n\nasync function resolveHttpUser(req) {\n const authResult = await verifyClientToken(req);\n if (authResult?.ok && authResult.user?.uid) {\n return { ok: true, uid: String(authResult.user.uid), mode: authResult.kind || \"auth\" };\n }\n if (ALLOW_DEV_ANON && ALLOW_DEV_HTTP_UID) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) return { ok: true, uid: `dev-${devUid.slice(0, 64)}`, mode: \"dev-header\" };\n }\n return { ok: false, reason: authResult?.reason || \"auth_required\" };\n}\n\n/**\n * Epistemic ledger ingest — Firebase uid OR valid transport token + X-Castle-Dev-Uid.\n * Prod disables CASTLE_ALLOW_DEV_HTTP_UID; LLM may still work while telemetry 401'd without this.\n */\nasync function resolveEpistemicIngestActor(req) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok && tok === REQUIRED_GATEWAY_TOKEN) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) {\n return { ok: true, uid: `gw-telemetry-${devUid.slice(0, 64)}`, mode: \"gateway-token-dev-uid\" };\n }\n return { ok: false, reason: \"dev_uid_required\" };\n }\n if (tok) {\n return { ok: false, reason: \"gateway_token_mismatch\" };\n }\n }\n const auth = await resolveHttpUser(req);\n if (auth.ok) return auth;\n return { ok: false, reason: auth.reason || \"auth_required\" };\n}\n\n/** Mesh actor: Firebase uid, dev header, or `guest:<uuid>` via header / `guestId` query (SSE cannot set headers). */\nasync function resolveMeshActor(req) {\n const auth = await resolveHttpUser(req);\n if (auth.ok) return { ok: true, clientUid: auth.uid, mode: auth.mode };\n if (ALLOW_MESH_GUEST) {\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const gid = String(req.headers?.[\"x-castle-guest-id\"] || q.searchParams.get(\"guestId\") || \"\").trim();\n if (gid) {\n const normalized = gid.startsWith(\"guest:\") ? gid.slice(0, 180) : `guest:${gid.slice(0, 160)}`;\n return { ok: true, clientUid: normalized, mode: \"guest-header\" };\n }\n }\n return { ok: false, reason: \"mesh_auth_required\" };\n}\n\n/** Live genesis runtime JSON (GET /rhizoh/genesis/runtime + SSE infra sampler). */\nasync function buildGenesisRuntimeSurfacePayloadLive() {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n let spiralWsActive = 0;\n if (spiralWssForGenesis) {\n for (const c of spiralWssForGenesis.clients) {\n if (c.readyState === 1) spiralWsActive += 1;\n }\n }\n const base = buildGenesisRuntimeSurfacePayload({\n infraMetrics,\n scoredHealth: scored,\n rhizohEnterpriseMetrics,\n mesh: meshContinuityAggregate(),\n workerHealth: worker,\n port: PORT,\n spiralWebSocketClientsActive: spiralWsActive\n });\n return {\n ...base,\n replayFingerprint: computeGenesisReplayFingerprintV0(base),\n gatewayCapabilities: buildGenesisGatewayCapabilitiesV0(),\n genesisStream: {\n schema: \"castle.genesis.stream_cursor.v0\",\n lastAcceptedSeq: getGenesisContinuitySeq(),\n clientIngress: {\n schema: GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n contract: \"castle.world_observation.ingress_envelope.v1\",\n postPath: rhizohRuntime.routes.genesisIngress,\n closure: getGenesisWorldObservationIngressStatusV0()\n },\n seqContinuity: getGenesisContinuitySeqAuditV0(),\n eventArchive: genesisContinuityEventArchiveEnabled()\n ? {\n enabled: true,\n projection: GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION,\n queryPath: rhizohRuntime.routes.genesisContinuityEvents\n }\n : { enabled: false },\n replayRouter: {\n schema: GENESIS_REPLAY_ROUTER_SCHEMA,\n queryPath: rhizohRuntime.routes.genesisReplay,\n queryHint: \"from=&to= or range=from-to; optional type=&checkpoints=0\",\n temporalDiffPath: rhizohRuntime.routes.genesisReplayDiff,\n temporalDiffSchema: GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA,\n equivalencePath: rhizohRuntime.routes.genesisReplayEquivalence,\n analyticsPath: rhizohRuntime.routes.genesisReplayAnalytics,\n evolutionPath: rhizohRuntime.routes.genesisReplayEvolution\n }\n }\n };\n}\n\nconst httpServer = createServer(async (req, res) => {\n const corsAllow = applyHttpCorsHeaders(req, res);\n if (req.method === \"OPTIONS\") {\n if (!corsAllow && req.headers?.origin) {\n console.warn(\"[GATEWAY_CORS] preflight rejected for origin:\", req.headers.origin);\n }\n res.writeHead(corsAllow ? 204 : 403);\n res.end();\n return;\n }\n\n const pathname = getHttpPathname(req);\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/genesis/__ping\") {\n sendJson(res, 200, {\n ok: true,\n pid: process.pid,\n entry: __filename,\n time: Date.now(),\n genesis: true\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/epistemic/__probe\") {\n sendJson(res, 200, {\n ok: true,\n epistemicIngestActorV2: true,\n routes: {\n seal: rhizohRuntime.routes.epistemicSeal,\n logsBatch: rhizohRuntime.routes.epistemicLogsBatch\n },\n hasGatewayToken: Boolean(REQUIRED_GATEWAY_TOKEN),\n gatewayTokenLen: REQUIRED_GATEWAY_TOKEN ? REQUIRED_GATEWAY_TOKEN.length : 0\n });\n return;\n }\n\n if (\n req.method === \"GET\" &&\n (pathname === \"/rhizoh/live/world-feed\" ||\n pathname === \"/rhizoh/live/sports-bundle\" ||\n pathname === \"/rhizoh/live/news-headlines\")\n ) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_live_world_feed:${ip}`, RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const out =\n pathname === \"/rhizoh/live/sports-bundle\"\n ? await handleLiveSportsBundleGetV0()\n : pathname === \"/rhizoh/live/news-headlines\"\n ? await handleLiveNewsHeadlinesGetV0(req)\n : await handleLiveWorldFeedGetV0(req);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/webhooks/stripe\") {\n await handleStripeMembershipWebhook(req, res);\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/billing/stripe/checkout\") {\n await handleStripeCheckoutCreate(req, res, { readHttpJson, resolveHttpUser, sendJson });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/live\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, live: false, dns: true, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n sendJson(res, 200, { ok: true, live: true, dns: true, service: \"castle-gateway\", ts: Date.now() });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/cors\") {\n const reqOrigin = normalizeHttpOrigin(req.headers?.origin);\n const allowOrigin = accessControlAllowOriginValue(req);\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n requestOrigin: reqOrigin || null,\n allowed: Boolean(allowOrigin),\n allowOrigin: allowOrigin || null,\n allowSetSize: httpCors.allowSet.size\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/ready\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, ready: false, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n try {\n const persistence = getFirebasePersistence();\n const llmProbe = rhizohLlmEnvConfigured();\n sendJson(res, 200, {\n ok: true,\n ready: true,\n dns: true,\n persistence: persistence.mode,\n service: \"castle-gateway\",\n gateway: \"ok\",\n llm: llmProbe ? \"ok\" : \"degraded\",\n spine: llmProbe ? \"ok\" : \"partial\"\n });\n } catch (e) {\n sendJson(res, 503, { ok: false, ready: false, reason: String(e?.message || e), service: \"castle-gateway\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/reality\") {\n const payload = buildRealityHealthPayload();\n sendJson(res, payload.ok ? 200 : 503, payload);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/substrate\") {\n const authority = buildGatewaySubstrateAuthoritySnapshotV0();\n const operational = buildSubstrateOperationalSnapshotV0();\n const drift = buildRealityHealthPayload();\n const ok = Boolean(authority.productionAuthoritySatisfied) && drift.ok;\n sendJson(res, ok ? 200 : 503, {\n ok,\n service: \"castle-gateway\",\n ts: Date.now(),\n authority,\n operational,\n drift: { ok: drift.ok, errors: drift.drift?.errors, warnings: drift.drift?.warnings }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/substrate/health\") {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const body = await readHttpJson(req);\n const uid = (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestClientSubstrateHealthV0(body, { uid, clientId: uid });\n if (!result.ok) {\n sendJson(res, 400, result);\n return;\n }\n recordSubstrateOtelEventV0(\"client_health_ingest\", { uid: String(uid).slice(0, 32) });\n sendJson(res, 200, result);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/deps\") {\n const t0 = Date.now();\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, {\n ok: false,\n dns: true,\n llm: false,\n firestore: false,\n phase: \"maintenance\",\n latencyMs: Date.now() - t0,\n service: \"castle-gateway\"\n });\n return;\n }\n const persistence = getFirebasePersistence();\n let firestoreOk = persistence.mode !== \"firebase\";\n let firestoreDetail = persistence.mode === \"file\" ? \"file_mode\" : \"not_applicable\";\n if (persistence.mode === \"firebase\" && persistence.db) {\n const pr = await probeFirestoreReachable(persistence.db, 2000);\n firestoreOk = pr.ok;\n firestoreDetail = pr.reason || (pr.ok ? \"reachable\" : \"fail\");\n } else if (persistence.mode === \"firebase\" && !persistence.db) {\n firestoreOk = false;\n firestoreDetail = \"firebase_not_initialized\";\n }\n const llmOk = rhizohLlmEnvConfigured();\n const overallOk = llmOk && firestoreOk;\n const latencyMs = Date.now() - t0;\n sendJson(res, 200, {\n ok: overallOk,\n dns: true,\n llm: llmOk,\n firestore: persistence.mode === \"firebase\" ? firestoreOk : false,\n persistence: persistence.mode,\n firestoreDetail,\n latencyMs,\n service: \"castle-gateway\",\n wsPort: PORT\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health\") {\n const persistence = getFirebasePersistence();\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n wsPort: PORT,\n persistence: persistence.mode,\n presenceMesh: \"sse\"\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/health\") {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n sendJson(res, 200, {\n ok: true,\n role: \"gateway\",\n queueLagMs: infraMetrics.queueLag,\n queueDepth: infraMetrics.queueDepth,\n errors: infraMetrics.errors,\n status: scored.status,\n score: scored.score,\n reasons: scored.reasons,\n worker: worker\n ? {\n status: worker.status,\n score: worker.score,\n reasons: worker.reasons\n }\n : null,\n timestamp: Date.now()\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisRuntime) {\n const payload = await buildGenesisRuntimeSurfacePayloadLive();\n sendJson(res, 200, payload);\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.genesisIngress) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const actor = await resolveMeshActor(req);\n const clientId = actor.ok ? actor.clientUid : (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestGenesisWorldObservationV0(body, { clientId });\n if (!result.ok) {\n const status =\n result.error === \"rate_limited\" || result.error === \"agent_spoke_throttled\"\n ? 429\n : result.error === \"payload_too_large\"\n ? 413\n : 400;\n sendJson(res, status, result);\n return;\n }\n sendJson(res, result.idempotent ? 200 : 202, result);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisStream) {\n const corsOrigin = accessControlAllowOriginValue(req);\n if (!corsOrigin && String(process.env.NODE_ENV || \"\").toLowerCase() === \"production\") {\n const og = String(req.headers?.origin || \"\").trim();\n if (og) {\n console.warn(\"[GATEWAY] genesis SSE: missing Access-Control-Allow-Origin for browser Origin (CORS whitelist mismatch):\", og);\n }\n }\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n res.write(\"retry: 3000\\n\\n\");\n res.write(\": genesis continuity stream\\n\\n\");\n res.write(\"event: genesis\\n\");\n res.write(`data: {\"booted\":true,\"pid\":${process.pid}}\\n\\n`);\n registerGenesisSseClient(res, req);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLatest) {\n const cp = getLatestGenesisSignedCheckpoint();\n if (!cp) {\n sendJson(res, 404, { ok: false, error: \"no_checkpoint_yet\", hint: \"wait_for_seq_interval_or_configure_signing_secret\" });\n return;\n }\n sendJson(res, 200, { ok: true, checkpoint: cp });\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(rhizohRuntime.routes.genesisCheckpointBySeqPrefix)) {\n const tail = pathname.slice(rhizohRuntime.routes.genesisCheckpointBySeqPrefix.length).split(\"/\")[0];\n const seq = parseInt(String(tail || \"\").trim(), 10);\n const out = await genesisCheckpointQueryBySeqV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"checkpoint_not_found\") {\n sendJson(res, 404, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointRange) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryRangeV0(from, to);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"range_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLineage) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const seq = parseInt(String(u.searchParams.get(\"seq\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryLineageV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"lineage_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisContinuityEvents) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const limit = parseInt(String(u.searchParams.get(\"limit\") || \"\").trim(), 10);\n const out = await queryGenesisContinuityEventArchiveV0(from, to, type, limit);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\" || out.error === \"event_archive_disabled\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"archive_file_too_large\") {\n sendJson(res, 413, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplay) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await resolveGenesisReplayRouterV1({\n from: fromSeq,\n to: toSeq,\n type,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayDiff) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const out = await computeGenesisReplayTemporalDiffV1({ from: fromSeq, to: toSeq, type });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEquivalence) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from1 = parseInt(String(u.searchParams.get(\"from1\") || \"\").trim(), 10);\n const to1 = parseInt(String(u.searchParams.get(\"to1\") || \"\").trim(), 10);\n const from2 = parseInt(String(u.searchParams.get(\"from2\") || \"\").trim(), 10);\n const to2 = parseInt(String(u.searchParams.get(\"to2\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await compareGenesisReplayEquivalenceV1({\n from1,\n to1,\n from2,\n to2,\n type,\n includeCheckpoints\n });\n if (\n !out.ok &&\n (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")\n ) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayAnalytics) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayAnalyticsV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEvolution) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const collapseWindows = parseInt(String(u.searchParams.get(\"collapseWindows\") || \"0\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayEvolutionV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n collapseWindows,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/metrics\") {\n res.writeHead(200, { \"Content-Type\": \"text/plain; version=0.0.4; charset=utf-8\" });\n res.end(renderInfraMetricsProm());\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.infraPrometheusMetrics) {\n if (process.env.CASTLE_PROMETHEUS_METRICS !== \"1\") {\n sendJson(res, 404, { ok: false, error: \"prometheus_metrics_disabled\" });\n return;\n }\n res.writeHead(200, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(renderRhizohPrometheusMetrics());\n return;\n }\n if (req.method === \"GET\" && pathname.startsWith(\"/infra/traces/\")) {\n const traceId = decodeURIComponent(pathname.slice(\"/infra/traces/\".length));\n const item = await getTraceById(traceId);\n if (!item) return sendJson(res, 404, { ok: false, error: \"trace_not_found\" });\n return sendJson(res, 200, { ok: true, trace: item });\n }\n if (req.method === \"GET\" && pathname === \"/infra/traces\") {\n const u = new URL(String(req.url || \"/infra/traces\"), `http://${req.headers.host || \"localhost\"}`);\n const sessionId = String(u.searchParams.get(\"sessionId\") || \"\").trim();\n const items = sessionId ? await listTracesBySession(sessionId) : [];\n return sendJson(res, 200, { ok: true, sessionId, traces: items });\n }\n\n /** Presence room mesh (C): JOIN / LEAVE / SNAPSHOT / DELTA / REPLAY (HTTP); SUBSCRIBE = SSE stream. */\n if (req.method === \"POST\" && pathname === \"/presence/mesh/join\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid, clientUid: actor.clientUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/leave\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshLeave(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/snapshot\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n sendJson(res, 200, meshSnapshot(roomUid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/delta\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, MAX_MESSAGE_BYTES);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const seq = meshAppendDelta(roomUid, actor.clientUid, {\n node: body?.node,\n projectionPatch: body?.projectionPatch,\n writerSubject: body?.writerSubject\n });\n sendJson(res, 200, { ok: true, roomUid, seq });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/replay\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const fromNodeId = body?.fromNodeId != null ? String(body.fromNodeId) : \"\";\n const fromSeq = body?.fromSeq;\n if (fromNodeId) {\n sendJson(res, 200, meshReplayFromNodeId(roomUid, fromNodeId));\n return;\n }\n sendJson(res, 200, meshReplayFromSeq(roomUid, fromSeq));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/presence/mesh/subscribe\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const roomUid = String(q.searchParams.get(\"roomUid\") || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n const write = (chunk) => {\n try {\n res.write(chunk);\n } catch {\n /* closed */\n }\n };\n const unsub = meshSubscribeSse(roomUid, write);\n const onClose = () => {\n unsub();\n meshLeave(roomUid, actor.clientUid);\n try {\n res.end();\n } catch {\n /* ignore */\n }\n };\n req.on(\"close\", onClose);\n req.on(\"aborted\", onClose);\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, uid: auth.uid, mode: auth.mode, items: await listConnections(auth.uid) });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const id = await createConnection(auth.uid, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"create_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n await updateConnection(auth.uid, id, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"update_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"DELETE\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n await deleteConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"delete_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections/test\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n if (!checkHttpRateLimit(`llm_conn_test:${auth.uid}`, RL_LLM_CONN_TEST_PER_MIN, 60_000)) {\n return sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n }\n const hasBodyKey = !!String(payload?.apiKey || \"\").trim();\n const result = await queryRhizohLlm(\n {\n message: String(payload?.message || \"Ping from Castle Gateway\"),\n context: payload?.context || { source: \"connection_test\" },\n provider: payload?.provider,\n model: payload?.model,\n apiKey: payload?.apiKey || \"\"\n },\n { allowExternalApiKey: true, llmKeySource: hasBodyKey ? \"auto\" : \"env\" }\n );\n logLlmAccess({\n route: \"/llm/connections/test\",\n uid: auth.uid,\n llmKeyBillingOwner: result.llmKeyBillingOwner,\n llmKeyOrigin: result.llmKeyOrigin,\n provider: result.provider,\n model: result.model,\n connectionId: null\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"connection_test_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAgentIdentities(auth.uid);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerAgentIdentity({\n ownerUid: auth.uid,\n agentId: payload?.agentId,\n role: payload?.role,\n personaSeed: payload?.personaSeed || {},\n capabilityLevel: payload?.capabilityLevel\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/agents/identities/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const owns = await getAgentIdentity(id);\n if (!owns || owns.ownerUid !== auth.uid) return sendJson(res, 403, { ok: false, error: \"forbidden\" });\n const payload = await readHttpJson(req);\n const row = await updateAgentIdentity(id, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/academy/events\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/academy/events\"}`);\n const status = String(url.searchParams.get(\"status\") || \"\");\n const items = await listAcademyEvents({ ownerUid: auth.uid, status, limit: 120 });\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/academy/events\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const ev = await queueAcademyEvent({\n type: payload?.type,\n roomId: payload?.roomId,\n ownerUid: auth.uid,\n participants: payload?.participants || [],\n topic: payload?.topic || \"academy\",\n payload: payload?.payload || {}\n });\n sendJson(res, 200, { ok: true, event: ev });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && String(req.url || \"\").startsWith(\"/academy/events/\") && String(req.url || \"\").endsWith(\"/resolve\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const parts = String(req.url || \"\").split(\"/\").filter(Boolean);\n const eventId = parts[2] || \"\";\n try {\n const payload = await readHttpJson(req);\n const out = await resolveAcademyEvent(eventId, payload || {});\n sendJson(res, 200, { ok: true, event: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_resolve_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/memory\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/memory\"}`);\n const scope = url.searchParams.get(\"scope\") === \"agents\" ? \"agents\" : \"users\";\n const id = String(url.searchParams.get(\"id\") || (scope === \"agents\" ? \"\" : auth.uid));\n const kind = String(url.searchParams.get(\"kind\") || \"\");\n const limit = Number(url.searchParams.get(\"limit\") || 40);\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n sendJson(res, 200, { ok: true, scope, id, kind, items: await listMemories({ scope, id, kind, limit }) });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const profile = await getPersonaGoalMemory(auth.uid);\n sendJson(res, 200, { ok: true, profile });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const profile = await setPersonaGoalMemory({\n uid: auth.uid,\n personaPatch: payload?.personaPatch || {},\n goals: Array.isArray(payload?.goals) ? payload.goals : null,\n preferencesPatch: payload?.preferencesPatch || {}\n });\n sendJson(res, 200, { ok: true, profile });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"profile_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/compact\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const out = await autoCompactMemories({ scope, id });\n sendJson(res, 200, { ok: true, scope, id, ...out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_compact_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/context\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const context = await getMemoryContext({\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n query: String(payload?.query || \"\"),\n limit: Number(payload?.limit || 80)\n });\n sendJson(res, 200, { ok: true, context });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_context_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/place-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const placeName = String(payload?.placeName || \"\").trim();\n if (!placeName) return sendJson(res, 400, { ok: false, error: \"placeName_required\" });\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const persona = String(payload?.waypointPersona || payload?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(placeName)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Place: ${placeName}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Give a concise conversational briefing about this place for a walking-route companion.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-place-brief\", placeName, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `PLACE_BRIEF:${placeName} => ${String(summary.reply || \"\").slice(0, 900)}`,\n tags: [\"place\", \"brief\", \"event-layer\"],\n importance: 0.62,\n meta: { placeName }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"place-brief\",\n text: `${placeName} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: {\n placeName,\n directive: summary.directive,\n lat: hit?.lat ? Number(hit.lat) : null,\n lon: hit?.lon ? Number(hit.lon) : null\n }\n });\n sendJson(res, 200, { ok: true, place: hit || null, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"place_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/route-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const waypoints = Array.isArray(payload?.waypoints) ? payload.waypoints.slice(0, 24) : [];\n const out = [];\n for (const wp of waypoints) {\n const name = String(wp?.name || \"\").trim();\n if (!name) continue;\n const persona = String(wp?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(name)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Waypoint: ${name}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Generate a concise spoken briefing for route waypoint.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-route-brief\", waypoint: name, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n out.push({\n name,\n persona,\n place: hit || null,\n reply: summary.reply,\n directive: summary.directive\n });\n }\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `ROUTE_BRIEF: waypoints=${out.length} ${out.map((x) => `${x.name}:${x.persona}`).join(\" | \")}`.slice(0, 1300),\n tags: [\"route\", \"waypoint\", \"brief\", \"event-layer\"],\n importance: 0.74,\n meta: { count: out.length }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"route-brief\",\n text: out.map((x) => `${x.name}:${String(x.reply || \"\").slice(0, 140)}`).join(\" | \").slice(0, 1400),\n roomId: \"studio-main\",\n meta: {\n waypointCount: out.length,\n waypoints: out.map((x) => ({\n name: x.name,\n persona: x.persona,\n lat: x.place?.lat ? Number(x.place.lat) : null,\n lon: x.place?.lon ? Number(x.place.lon) : null\n }))\n }\n });\n sendJson(res, 200, { ok: true, items: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"route_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/pdf-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const title = String(payload?.title || \"document\");\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const providedText = String(payload?.text || \"\");\n const url = String(payload?.url || \"\");\n let content = providedText;\n if (!content && url) content = await fetchTextFromUrl(url);\n if (!content) return sendJson(res, 400, { ok: false, error: \"text_or_supported_url_required\" });\n const summary = await queryRhizohLlm(\n {\n message: `Summarize this document and provide key points + practical actions.\\nTitle:${title}\\nContent:\\n${content.slice(0, 12000)}`,\n context: { source: \"event-layer-pdf-brief\", title },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `DOC_BRIEF:${title} => ${String(summary.reply || \"\").slice(0, 1000)}`,\n tags: [\"document\", \"brief\", \"library\", \"event-layer\"],\n importance: 0.7,\n meta: { title, url: url || null }\n });\n await addTranscript(auth.uid, {\n source: \"library-brief\",\n eventType: \"pdf-brief\",\n text: `${title} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { title }\n });\n sendJson(res, 200, { ok: true, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/pdf/upload\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req, 8 * 1024 * 1024);\n const job = await enqueuePdfJob({\n uid: auth.uid,\n fileName: payload?.fileName || \"document.pdf\",\n bytesBase64: payload?.contentBase64\n });\n sendJson(res, 200, { ok: true, job });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_upload_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/session\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createPublishSession(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session\",\n text: `Publish session armed: ${row.protocol} ${row.target}`.slice(0, 500),\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: row.id, bridge: row.bridge }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/studio/publish/sessions\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listPublishSessions(auth.uid, 80);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/health-check\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const protocol = String(payload?.protocol || \"WHIP\");\n const target = String(payload?.target || \"\");\n const health = await checkPublishTargetHealth(protocol, target);\n if (payload?.sessionId) {\n await updatePublishSession(auth.uid, String(payload.sessionId), {\n status: health.ok ? \"healthy\" : \"degraded\",\n reconnectPolicy: {\n enabled: true,\n backoffSec: [2, 5, 10, 20],\n nextRetryInSec: health.reconnectInSec || 0,\n maxRetries: 8,\n reason: health.reason\n }\n });\n }\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-health\",\n text: `${protocol} ${target} => ${health.ok ? \"healthy\" : \"degraded\"} (${health.reason})`,\n roomId: \"studio-main\",\n meta: { protocol, target, reconnect: !!health.shouldReconnect }\n });\n sendJson(res, 200, { ok: true, health });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_health_check_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/studio/publish/sessions/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updatePublishSession(auth.uid, id, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session-update\",\n text: `Session ${id} -> ${row.status || \"updated\"}`,\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: id, status: row.status, ingestUrl: row.ingestUrl || \"\" }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/transcripts\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/transcripts\"}`);\n const limit = Math.max(1, Math.min(200, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await listTranscripts(auth.uid, limit);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/transcripts\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await addTranscript(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"transcript_write_failed\" });\n }\n return;\n }\n\n /** E1: broadcast intent → GreenRoom capability → transcript + memory artifact */\n if (req.method === \"POST\" && req.url === \"/studio/capabilities/greenroom\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const traceId = String(payload?.traceId || `TRC-${Date.now().toString(36).toUpperCase()}`);\n const title = String(payload?.title || \"Castle Live Broadcast\").slice(0, 200);\n const intentRaw = String(payload?.intentRaw || \"\").slice(0, 800);\n const audienceEstimate = Math.max(0, Math.min(50_000_000, Number(payload?.audienceEstimate) || 1200));\n const roomId = String(payload?.roomId || \"greenroom-main\");\n const transcriptRow = await addTranscript(auth.uid, {\n source: \"rhizoh\",\n eventType: \"BROADCAST_ROUTED\",\n text: `GreenRoom · ${title}${intentRaw ? `\\nIntent: ${intentRaw}` : \"\"}`,\n roomId,\n meta: {\n ack: \"BROADCAST_ROUTED\",\n capability: \"greenroom\",\n intentType: \"broadcast\",\n traceId,\n audienceEstimate,\n artifact: {\n kind: \"broadcast_card\",\n title,\n traceId,\n roomId,\n createdAt: Date.now()\n }\n }\n });\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n text: `GREENROOM_BROADCAST:${title} trace=${traceId}`,\n tags: [\"broadcast\", \"greenroom\", \"artifact\", \"castle-library\"],\n importance: 0.72,\n kind: \"episodic\",\n meta: { traceId, capability: \"greenroom\", transcriptId: transcriptRow.id }\n });\n sendJson(res, 200, {\n ok: true,\n ack: \"BROADCAST_ROUTED\",\n traceId,\n transcript: transcriptRow,\n replayPath: `/replay/${encodeURIComponent(traceId)}`,\n sharePath: `/replay/${encodeURIComponent(traceId)}`\n });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"greenroom_capability_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/metadata/search\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/metadata/search\"}`);\n const q = String(url.searchParams.get(\"q\") || \"\");\n const limit = Math.max(1, Math.min(300, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await searchMetadata(auth.uid, q, limit);\n sendJson(res, 200, { ok: true, q, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerDevice(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, device: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"device_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const lat = Number(payload?.lat);\n const lon = Number(payload?.lon);\n const speed = Math.max(0, Number(payload?.speed || 0));\n const cfgCenterLat = Number(process.env.CASTLE_GEOFENCE_CENTER_LAT || 41.0082);\n const cfgCenterLon = Number(process.env.CASTLE_GEOFENCE_CENTER_LON || 28.9784);\n const cfgRadiusM = Math.max(100, Number(process.env.CASTLE_GEOFENCE_RADIUS_M || 55000));\n const cfgSpeedCap = Math.max(1, Number(process.env.CASTLE_SPEED_CAP_MPS || 35));\n const dist = Number.isFinite(lat) && Number.isFinite(lon) ? haversineMeters(cfgCenterLat, cfgCenterLon, lat, lon) : 0;\n const geofenceOk = !Number.isFinite(dist) ? true : dist <= cfgRadiusM;\n const speedOk = speed <= cfgSpeedCap;\n const failsafe = !(geofenceOk && speedOk);\n const safety = {\n geofenceOk,\n speedOk,\n failsafe,\n distanceFromCenterM: Number.isFinite(dist) ? dist : null,\n speedCapMps: cfgSpeedCap\n };\n const row = await appendTelemetry(auth.uid, deviceId, {\n lat,\n lon,\n speed,\n battery: payload?.battery,\n mode: payload?.mode || (failsafe ? \"FAILSAFE_HOLD\" : \"ACTIVE\"),\n raw: payload?.raw || {}\n });\n await updateDevice(auth.uid, deviceId, {\n status: failsafe ? \"failsafe\" : \"active\",\n lastTelemetryAt: row.ts,\n safetyState: safety\n });\n if (failsafe) {\n await addTranscript(auth.uid, {\n source: \"robotics-guardrails\",\n eventType: \"failsafe\",\n text: `Device ${deviceId} failsafe triggered geofenceOk=${geofenceOk} speedOk=${speedOk}`,\n roomId: \"studio-main\",\n meta: { deviceId, safety }\n });\n }\n sendJson(res, 200, { ok: true, telemetry: row, safety, command: failsafe ? \"FAILSAFE_HOLD\" : \"CONTINUE\" });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"telemetry_ingest_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listTelemetry(auth.uid, 150);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createAutomation(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"automation-scheduled\",\n text: `Automation scheduled: ${row.title} at ${new Date(row.scheduleAt).toLocaleString()}`,\n roomId: \"studio-main\",\n meta: { automationId: row.id, action: row.action, deviceId: row.deviceId }\n });\n sendJson(res, 200, { ok: true, automation: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"automation_create_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAutomations(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listDevices(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const devs = await listDevices(auth.uid, 500);\n const d = devs.find((x) => x.id === deviceId);\n if (!d) return sendJson(res, 404, { ok: false, error: \"device_not_found\" });\n const action = String(payload?.action || \"\");\n const caps = Array.isArray(d.capabilityProfile) ? d.capabilityProfile : [];\n if (action && caps.length && !caps.includes(action) && !caps.includes(\"*\")) {\n return sendJson(res, 400, { ok: false, error: \"unsupported_action_for_device\", capabilityProfile: caps });\n }\n const cmd = await enqueueCommand(auth.uid, {\n deviceId,\n action: action || \"NOOP\",\n params: payload?.params || {},\n adapter: payload?.adapter || d.adapter || \"websocket\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-command-queue\",\n eventType: \"command-queued\",\n text: `${cmd.deviceId} -> ${cmd.action}`,\n roomId: \"studio-main\",\n meta: { commandId: cmd.id, adapter: cmd.adapter }\n });\n sendJson(res, 200, { ok: true, command: cmd });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listCommands(auth.uid, 200);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/robotics/commands/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updateCommandStatus(auth.uid, id, payload?.status || \"sent\");\n sendJson(res, 200, { ok: true, command: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/rhizoh/bridge\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const result = await runRhizohBrainV2({\n message: String(payload?.message || \"robotics bridge command\"),\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n worldState: payload?.worldState || {},\n deviceState: payload?.deviceState || {},\n intent: \"robotics-mechanics\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-bridge\",\n eventType: \"robotics-command\",\n text: `Bridge -> ${String(result.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { directive: result.directive, agentId: payload?.agentId || \"\" }\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"robotics_bridge_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/planner/graph\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const graph = buildTaskActionGraph(String(payload?.task || \"\"), payload?.context || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"planner-graph\",\n text: `Planner graph built: ${graph.taskTitle} nodes=${graph.nodes.length}`,\n roomId: \"studio-main\",\n meta: { taskTitle: graph.taskTitle, nodeCount: graph.nodes.length }\n });\n sendJson(res, 200, { ok: true, graph });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"planner_graph_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerSocialChannel(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, channel: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_channel_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listSocialChannels(auth.uid, 100);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/broadcast\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const text = String(payload?.text || \"\").slice(0, 1200);\n const channels = await listSocialChannels(auth.uid, 100);\n const enabled = channels.filter((c) => c.status === \"enabled\");\n const deliveries = [];\n for (const c of enabled) {\n const out = await deliverSocialWithRetry(c, text);\n deliveries.push({ id: c.id, platform: c.platform, ...out });\n }\n const okCount = deliveries.filter((d) => d.ok).length;\n await addTranscript(auth.uid, {\n source: \"social-layer\",\n eventType: \"social-broadcast\",\n text: `Broadcast delivered ${okCount}/${enabled.length} channel(s): ${text.slice(0, 240)}`,\n roomId: \"studio-main\",\n meta: { deliveries }\n });\n sendJson(res, 200, { ok: true, sentChannels: okCount, totalChannels: enabled.length, deliveries });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_broadcast_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/ethics/programs\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, ...buildEthicsPrograms() });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands/dispatch\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const all = await listCommands(auth.uid, 200);\n const queued = all.filter((c) => String(c.status || \"queued\") === \"queued\").slice(0, 50);\n const devices = await listDevices(auth.uid, 500);\n const byId = new Map(devices.map((d) => [d.id, d]));\n const results = [];\n for (const cmd of queued) {\n const dev = byId.get(cmd.deviceId);\n if (!dev) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: \"device_not_found\" });\n continue;\n }\n try {\n const dispatched = await dispatchCommandToAdapter(dev, cmd);\n await updateCommandStatus(auth.uid, cmd.id, \"sent\");\n results.push({ id: cmd.id, ok: true, adapter: dispatched.adapter, via: dispatched.via });\n } catch (error) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: String(error?.message || \"dispatch_failed\") });\n }\n }\n await addTranscript(auth.uid, {\n source: \"robotics-command-dispatch\",\n eventType: \"command-dispatch-batch\",\n text: `Dispatch batch size=${results.length} ok=${results.filter((x) => x.ok).length}`,\n roomId: \"studio-main\",\n meta: { results: results.slice(0, 30) }\n });\n sendJson(res, 200, { ok: true, processed: results.length, results });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"dispatch_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/pdf/jobs/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n const job = await getPdfJob(auth.uid, id);\n if (!job) return sendJson(res, 404, { ok: false, error: \"pdf_job_not_found\" });\n sendJson(res, 200, { ok: true, job });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const row = await appendMemory({\n scope,\n id,\n text: payload?.text,\n tags: payload?.tags || [],\n importance: payload?.importance,\n kind: payload?.kind || \"episodic\",\n meta: { uid: auth.uid, ...(payload?.meta || {}) }\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.externalTruth) {\n try {\n if (process.env.RHIZOH_EXTERNAL_TRUTH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_truth:${ip}`, RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n sendJson(res, 200, buildRhizohExternalGroundTruthPayload(req));\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.productOutcome) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const rawBody = await readHttpBodyText(req, 64 * 1024);\n const body = rawBody.trim() ? safeJsonParse(rawBody) : null;\n if (!body || typeof body !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"invalid_json_payload\" });\n return;\n }\n const sigHdr = String(req.headers?.[\"x-rhizoh-outcome-signature\"] || \"\").trim();\n const hmacV = verifyRhizohOutcomeHmac(rawBody, sigHdr);\n if (!hmacV.ok) {\n sendJson(res, 401, { ok: false, error: hmacV.error || \"outcome_hmac_invalid\" });\n return;\n }\n const srcTokHdr = String(req.headers?.[\"x-rhizoh-outcome-source-token\"] || \"\").trim();\n const srcV = verifyRhizohOutcomeSourceToken(body.source, srcTokHdr);\n if (!srcV.ok) {\n sendJson(res, 401, { ok: false, error: srcV.error || \"outcome_source_token_invalid\" });\n return;\n }\n const subjectKey = String(body.subjectKey || \"\").trim().slice(0, 128);\n if (\n subjectKey &&\n !checkHttpRateLimit(\n `rhizoh_product_outcome_subj:${subjectKey}`,\n RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN,\n 60_000\n )\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_subject\" });\n return;\n }\n const result = ingestRhizohProductOutcomeHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n schemaVersion: \"1.1.0\",\n aggregate: result.aggregate,\n integrity: result.integrity\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"outcome_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.productOutcomeAggregate) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome_agg:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const u = new URL(String(req.url || \"/\"), \"http://localhost\");\n const cohortId = u.searchParams.get(\"cohortId\") || \"\";\n const decisionFingerprint = u.searchParams.get(\"decisionFingerprint\") || \"\";\n const limit = u.searchParams.get(\"limit\") || \"50\";\n const rows = listRhizohProductOutcomeAggregates({\n cohortId: cohortId || undefined,\n decisionFingerprint: decisionFingerprint || undefined,\n limit: Number(limit) || 50\n });\n sendJson(res, 200, { ok: true, schemaVersion: \"1.1.0\", rows });\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.externalLossBatch) {\n try {\n if (process.env.RHIZOH_EXTERNAL_LOSS_BATCH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_loss_batch:${ip}`, RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const result = ingestRhizohExternalLossBatchHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, { ok: true, accepted: result.accepted ?? 0 });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"batch_failed\") });\n }\n return;\n }\n\n function moderationAdminOk(req) {\n const expected = String(process.env.CASTLE_MODERATION_ADMIN_KEY || \"\").trim();\n if (!expected) return false;\n const got = String(req.headers[\"x-castle-moderation-key\"] || \"\").trim();\n return got.length > 0 && got === expected;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.opsAbuseReport) {\n try {\n const body = await readHttpJson(req, 32 * 1024);\n const auth = await resolveHttpUser(req);\n const entry = submitAbuseReportV0({\n reporterUid: auth.ok ? auth.uid : null,\n targetUid: body?.targetUid ?? null,\n category: body?.category,\n detail: body?.detail,\n traceId: body?.traceId ?? null\n });\n sendJson(res, 202, { ok: true, reportId: entry.id, status: entry.status });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsModerationQueue) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n queue: listModerationQueueV0(100),\n softBlocked: listSoftBlockedUsersV0()\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-block\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const out = softBlockUserV0(body?.uid, body?.reason);\n sendJson(res, out.ok ? 200 : 400, out);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-unblock\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n sendJson(res, 200, removeSoftBlockV0(body?.uid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsAgentSnapshots) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n const limit = Number(new URL(req.url || \"\", \"http://local\").searchParams.get(\"limit\") || 50);\n sendJson(res, 200, { ok: true, snapshots: listRecentAgentSnapshotsV0(limit) });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.academicObservatoryExport) {\n const auth = await resolveHttpUser(req);\n const out = handleAcademicObservatoryExportGetV0(req, auth);\n sendJson(res, out.status, out.body);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsHardeningStatus) {\n const auth = await resolveHttpUser(req);\n const principal = auth.ok ? `uid:${auth.uid}` : null;\n const gclSnap = await getGlobalCostLedgerSnapshotV0(principal);\n const economicStrategy = evaluateEconomicStrategyL3V0(gclSnap);\n const hardeningTenantId = resolveHardeningTenantIdV0(auth, req);\n const unifiedState = await buildUnifiedStateNarrativeV0({\n tenantId: hardeningTenantId ?? undefined,\n platformScope: !hardeningTenantId,\n principal\n });\n sendJson(res, 200, {\n ok: true,\n schema: \"rhizoh.ops.hardening_status.v0\",\n containment: readAgentContainmentConfigV0(),\n cost: readCostContainmentConfigV0(),\n gcl: {\n backend: await getGclBackendStatusV0(),\n snapshot: gclSnap\n },\n economicStrategyL3: economicStrategy,\n phasedRollout: await getPhasedRolloutStatsV0(),\n tier: readPhasedRolloutTierV0(),\n principalCost: auth.ok ? await getCostContainmentStatsV0(principal) : null,\n stressTaxonomy: verifyStressTaxonomyCoverageV0(),\n stressConflictResolution: verifyStressConflictResolutionV0(),\n resolutionStabilityEnvelope: verifyResolutionStabilityEnvelopeV0(),\n unifiedState: {\n tenantScope: {\n tenantId: unifiedState.tenantScope?.tenantId,\n scope: unifiedState.tenantScope?.scope,\n noGlobalDerivedState: unifiedState.tenantScope?.noGlobalDerivedState\n },\n narrativeFingerprint: unifiedState.narrativeFingerprint,\n screenshotScopeWatermark: unifiedState.screenshotScopeWatermark,\n interpretationSafetyContract: unifiedState.interpretationSafetyContract,\n interpretationUxContract: {\n schema: unifiedState.interpretationUxContract?.schema,\n layerOrder: unifiedState.interpretationUxContract?.layerOrder,\n narrativeNeverVisuallyDominant: unifiedState.interpretationUxContract?.narrativeNeverVisuallyDominant\n },\n governance: {\n narrativeIsNotDecisionLayer: unifiedState.governance.narrativeIsNotDecisionLayer,\n displayOrder: unifiedState.governance.displayOrder,\n trustPosture: unifiedState.governance.trustPosture,\n interpretationUxContract: unifiedState.governance.interpretationUxContract?.schema\n },\n humanOps: {\n decisionTier: unifiedState.humanOps?.humanDecisionScaling?.tier,\n decisionOwner: unifiedState.humanOps?.humanDecisionScaling?.decisionOwner,\n opsRouteId: unifiedState.humanOps?.humanDecisionOpsRunbook?.routing?.routeId,\n slaAckMinutes: unifiedState.humanOps?.humanDecisionOpsRunbook?.sla?.ackMinutes,\n uiDisclaimerRequired: unifiedState.humanOps?.narrativeUiSafety?.displayRules?.requireDisclaimerBanner,\n misreadHighResidual: unifiedState.humanOps?.misreadSimulation?.highResidualCount,\n propagationHighResidual: unifiedState.humanOps?.socialPropagationSimulation?.highResidualCount,\n worstWatermarkSurvivability:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.worstWatermarkSurvivability,\n dominantDistortionSource:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.dominantDistortionSource\n },\n culturalRisk: {\n perceptualRiskClass: unifiedState.culturalRisk?.perceptualRiskClass,\n trustFork: unifiedState.culturalRisk?.trustDynamics?.fork,\n trustDecayScore: unifiedState.culturalRisk?.trustDynamics?.scores?.trustDecay,\n mythologyScore: unifiedState.culturalRisk?.trustDynamics?.scores?.mythology,\n confidenceDetachmentExposure:\n unifiedState.culturalRisk?.trustDynamics?.axes?.confidenceDetachmentExposure\n },\n appliedSystemsLayer: {\n schema: unifiedState.appliedSystemsLayer?.schema,\n researchOutputType: unifiedState.appliedSystemsLayer?.researchGovernance?.outputType,\n roboticsBlockActuation: unifiedState.appliedSystemsLayer?.roboticsGrounding?.blockActuation,\n spiralRumorCount:\n unifiedState.appliedSystemsLayer?.spiralMMOGameKernel?.gameLayer?.rumors?.length\n },\n actionSemanticGovernance: {\n schema: unifiedState.actionSemanticGovernance?.schema,\n highRiskActionCount: unifiedState.actionSemanticGovernance?.highRiskActionCount,\n loopClosed: unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.loopClosed,\n authorityCollapseMitigated:\n unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.risks?.authorityCollapse\n ?.mitigated\n },\n actionContextResolution: {\n schema: unifiedState.actionContextResolution?.schema,\n contextFingerprint: unifiedState.actionContextResolution?.contextFingerprint?.fingerprintHash,\n domainCollision: unifiedState.actionContextResolution?.domainCollision?.detected,\n deployAgentSpiralBlocked:\n unifiedState.actionContextResolution?.actionOverloadingRisk?.deployAgentExample\n ?.spiral_mmo === \"blocked\",\n roboticsActuationEligible:\n unifiedState.actionContextResolution?.roboticsActuationCorrectness?.actuationEligible\n },\n epistemicCoherence: {\n schema: unifiedState.epistemicCoherence?.schema,\n verdict: unifiedState.epistemicCoherence?.systemCoherence?.verdict,\n coherenceScore: unifiedState.epistemicCoherence?.systemCoherence?.score,\n contradictionCount: unifiedState.epistemicCoherence?.crossLayerContradictions?.count,\n operatorDecision: unifiedState.epistemicCoherence?.uxCompression?.operatorDecision,\n headlineTr: unifiedState.epistemicCoherence?.uxCompression?.headline?.tr,\n falseSafetyWarning: Boolean(\n unifiedState.epistemicCoherence?.uxCompression?.falseSafetyWarning\n )\n },\n coherenceAuthorityBoundary: {\n schema: unifiedState.coherenceAuthorityBoundary?.schema,\n bindingValid: unifiedState.coherenceAuthorityBoundary?.eclAuthorityBinding?.valid,\n canExecute: unifiedState.coherenceAuthorityBoundary?.contract?.can_execute,\n mandatoryBannerTr:\n unifiedState.coherenceAuthorityBoundary?.disclaimers?.mandatoryBanner?.tr\n },\n decisionLatencyGovernance: {\n schema: unifiedState.decisionLatencyGovernance?.schema,\n latencyTier: unifiedState.decisionLatencyGovernance?.latencyTier,\n inflationScore: unifiedState.decisionLatencyGovernance?.latencyInflation?.score,\n fastPathEligible: unifiedState.decisionLatencyGovernance?.fastPath?.eligible,\n primaryActionId: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.primaryActionId,\n ackSlaMinutes: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.ackSlaMinutes,\n certaintyCap: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.certaintyCap,\n uncertaintyItems:\n unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.uncertaintyEnvelope?.itemCount\n },\n decisionPacketUncertaintyBoundary: {\n schema: unifiedState.decisionPacketUncertaintyBoundary?.schema,\n invariantValid: unifiedState.decisionPacketUncertaintyBoundary?.invariantCheck?.valid,\n centralInvariant: unifiedState.decisionPacketUncertaintyBoundary?.centralInvariant\n },\n epistemicDecisionPacing: {\n schema: unifiedState.epistemicDecisionPacing?.schema,\n queueSaturationTier:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation?.tier,\n operatorProcessingLatency:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation\n ?.operatorProcessingLatency,\n postureWindowCount:\n unifiedState.epistemicDecisionPacing?.temporaryStaticPostureWindows\n ?.temporaryStaticPostureWindows?.windowCount,\n executionBoundariesValid:\n unifiedState.epistemicDecisionPacing?.executionBoundaries?.valid,\n grantsExecutionApproval: unifiedState.epistemicDecisionPacing?.grantsExecutionApproval\n },\n epistemicTemporalCoherence: {\n schema: unifiedState.epistemicTemporalCoherence?.schema,\n allInvariantsValid: unifiedState.epistemicTemporalCoherence?.allInvariantsValid,\n temporalAlignmentValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.temporalAlignment?.valid,\n crossWindowGuardValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard?.valid,\n delayedTruthValid: unifiedState.epistemicTemporalCoherence?.invariants?.delayedTruthRule?.valid,\n reconciliationRequired:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard\n ?.reconciliation?.required\n },\n realityDriftObserver: {\n schema: unifiedState.realityDriftObserver?.schema,\n driftActive: unifiedState.realityDriftObserver?.driftObserverSummary?.active,\n misapprehensionShapeCount:\n unifiedState.realityDriftObserver?.misapprehensionShapeCatalog?.shapeCount,\n criticalShapeCount:\n unifiedState.realityDriftObserver?.driftObserverSummary?.criticalShapeCount,\n roboticsMismatchActive:\n unifiedState.realityDriftObserver?.roboticsFeedbackMismatch?.active\n },\n driftCausality: {\n schema: unifiedState.driftCausality?.schema,\n shapesWithCausalExplanation: unifiedState.driftCausality?.rdolLinkage?.shapesWithCausalExplanation,\n activeDomainCount: unifiedState.driftCausality?.causalitySummary?.activeDomainCount,\n propagationCausalityActive: unifiedState.driftCausality?.causalDomains?.propagation?.active\n },\n productReadiness: unifiedState.productReadiness,\n stateLayers: {\n raw: unifiedState.stateLayers.raw,\n derived: {\n systemState: unifiedState.stateLayers.derived.systemState,\n narrative: unifiedState.stateLayers.derived.narrative,\n validation: unifiedState.stateLayers.derived.validation\n },\n policy: {\n binding: unifiedState.stateLayers.policy.binding,\n decisionOwner: unifiedState.stateLayers.policy.decisionOwner,\n prohibitedActions: unifiedState.stateLayers.policy.prohibitedActions\n }\n },\n systemState: unifiedState.systemState,\n interpretation: unifiedState.interpretation,\n validation: {\n trustPosture: unifiedState.validation.trustPosture,\n adjustedConfidence: unifiedState.validation.adjustedConfidence,\n agreement: unifiedState.validation.confidenceDecomposition.agreement,\n divergenceScore: unifiedState.validation.divergence.divergenceScore,\n validated: unifiedState.validation.divergence.validated,\n uncertaintyTags: unifiedState.validation.uncertainty.tags.map((t) => t.id),\n flags: unifiedState.validation.divergence.flags.map((f) => f.id)\n },\n compression: unifiedState.compression\n }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 6 * 1024 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (\n !checkHttpRateLimit(`rhizoh_voice_transcribe:${rlKey}`, RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN, 60_000)\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const voiceEnv = rhizohVoiceTranscribeEnvV3();\n if (!voiceEnv.any) {\n sendJson(res, 503, {\n ok: false,\n error: \"voice_asr_not_configured\",\n env: voiceEnv\n });\n return;\n }\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n const result = await runRhizohVoiceTranscribeV3(\n applyLanguagePropagationToVoiceBodyV1(payload, langProp)\n );\n sendJson(res, result.ok ? 200 : 422, {\n ...result,\n traceId: String(payload?.traceId || \"\"),\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n atMs: Date.now()\n });\n } catch (error) {\n sendJson(res, 500, {\n ok: false,\n error: \"voice_transcribe_failed\",\n detail: String(error?.message || error),\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === rhizohRuntime.routes.rhizohLlm) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 128 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n logRhizohHealth(\"gateway_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: String(payload?.traceId || \"\"),\n auth: auth.ok ? \"ok\" : \"anon\",\n ip\n });\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (!checkHttpRateLimit(`rhizoh_llm:${rlKey}`, RL_RHIZOH_LLM_PER_MIN, 60_000)) {\n const rlTaxonomy = classifyStressResponseV0({ code: \"rate_limit_exceeded\" });\n return sendJson(res, 429, {\n ok: false,\n error: \"rate_limit_exceeded\",\n reply: rlTaxonomy.userMessageTr || \"İstek sınırı aşıldı. Kısa süre sonra tekrar deneyin.\",\n stressClass: rlTaxonomy.stressClass,\n responseAction: rlTaxonomy.responseAction,\n stressMatrix: rlTaxonomy.matrix,\n stressInterpretable: rlTaxonomy.interpretable,\n stressConfidence: rlTaxonomy.stressConfidence,\n actionConfidence: rlTaxonomy.actionConfidence,\n actionSoftened: rlTaxonomy.actionSoftened,\n responseActionStrict: rlTaxonomy.responseActionStrict,\n actionInterpretable: rlTaxonomy.actionInterpretable,\n conflictResolution: rlTaxonomy.conflictResolution,\n directive: \"NONE\"\n });\n }\n\n const requireExplicit = process.env.CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE === \"1\";\n const keyMode = normalizeClientLlmKeySource(payload?.llmKeySource ?? payload?.keySource);\n if (keyMode === null) {\n return sendJson(res, 400, {\n ok: false,\n error: \"invalid_llm_key_source\",\n reply: \"llmKeySource geçersiz. Kullanın: env | user_connection | auto\",\n directive: \"NONE\"\n });\n }\n if (requireExplicit && keyMode === \"auto\") {\n return sendJson(res, 400, {\n ok: false,\n error: \"llm_key_source_required\",\n reply: \"Sunucu CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE=1: istekte llmKeySource (env veya user_connection) zorunlu.\",\n directive: \"NONE\"\n });\n }\n\n const conn = auth.ok ? await resolveConnection(auth.uid, String(payload?.connectionId || \"\")) : null;\n\n if (keyMode === \"user_connection\") {\n if (!auth.ok) {\n return sendJson(res, 401, {\n ok: false,\n error: \"llm_auth_required_for_user_connection\",\n reply: \"Kayıtlı LLM bağlantısı için giriş gerekli.\",\n directive: \"NONE\"\n });\n }\n if (!conn?.apiKey) {\n return sendJson(res, 400, {\n ok: false,\n error: \"user_llm_connection_required\",\n reply: \"Geçerli bir LLM bağlantısı yok; Studio’da bağlantı ekleyin veya llmKeySource=env kullanın.\",\n directive: \"NONE\"\n });\n }\n }\n\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n let safePayload = applyLanguagePropagationToLlmPayloadV1({ ...(payload || {}) }, langProp);\n const trimOut = trimRhizohLlmGatewayPayloadV1(safePayload);\n if (trimOut.meta.trimmed) {\n logRhizohHealth(\"llm_payload_trimmed\", {\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n bytesBefore: trimOut.meta.bytesBefore,\n bytesAfter: trimOut.meta.bytesAfter\n });\n }\n safePayload = trimOut.payload;\n delete safePayload.apiKey;\n delete safePayload.llmKeySource;\n delete safePayload.keySource;\n\n let resolvedProvider = payload?.provider;\n let resolvedModel = payload?.model;\n let connApiKey = \"\";\n\n if (keyMode === \"user_connection\") {\n resolvedProvider = conn.provider;\n resolvedModel = conn.model;\n connApiKey = conn.apiKey || \"\";\n } else if (keyMode === \"env\") {\n connApiKey = \"\";\n } else {\n resolvedProvider = conn?.provider || payload?.provider;\n resolvedModel = conn?.model || payload?.model;\n connApiKey = conn?.apiKey || \"\";\n }\n\n const { result, traceId, turnLatencyMs, spinePhases, sampledTrace } = await rhizohGatewayTurn({\n safePayload,\n auth,\n keyMode,\n conn,\n resolvedProvider,\n resolvedModel,\n connApiKey\n });\n logRhizohHealth(\"llm_response\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId,\n turnLatencyMs: Number(turnLatencyMs || 0),\n keyMode\n });\n sendJson(res, 200, {\n ...result,\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n traceId,\n turnLatencyMs,\n ...(spinePhases ? { spinePhases } : {}),\n ...(sampledTrace !== undefined ? { sampledTrace } : {})\n });\n } catch (error) {\n const msg = String(error?.message || \"\");\n const code = error?.code || \"\";\n let status = 500;\n if (msg === \"payload_too_large\") status = 413;\n else if (msg === \"rate_limit_exceeded\") status = 429;\n else if (\n code === \"cost_hard_limit\" ||\n code === \"phased_rollout_capacity\" ||\n error?.containment === true ||\n String(code).startsWith(\"agent_\")\n ) {\n status = 429;\n } else if (code === \"user_soft_blocked\" || code === \"prompt_abuse_detected\") status = 403;\n else if ([\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(code) || msg.includes(\"missing_api_key_for_\"))\n status = 400;\n let rhizohFailureKind = \"provider_error\";\n if (msg === \"rate_limit_exceeded\" || status === 429) rhizohFailureKind = \"rate_limit\";\n else if (status === 403) rhizohFailureKind = \"policy_block\";\n else if (status === 400 || status === 413) rhizohFailureKind = \"client_config\";\n else if (msg.startsWith(\"provider_http_\")) {\n const st = Number(msg.replace(\"provider_http_\", \"\"));\n if (st === 408 || st === 504) rhizohFailureKind = \"timeout\";\n else rhizohFailureKind = \"provider_error\";\n } else if (/timeout|timed out|ETIMEDOUT/i.test(msg)) rhizohFailureKind = \"timeout\";\n\n const providerHttpMatch = msg.match(/^provider_http_(\\d{3})$/);\n const stressTaxonomy =\n error?.stressClass != null\n ? {\n stressClass: error.stressClass,\n responseAction: error.responseAction,\n stressMatrix: error.stressMatrix,\n stressInterpretable: error.stressInterpretable !== false,\n stressConfidence: error.stressConfidence,\n actionConfidence: error.actionConfidence,\n actionSoftened: error.actionSoftened,\n responseActionStrict: error.responseActionStrict,\n actionInterpretable: error.actionInterpretable,\n conflictResolution: error.conflictResolution,\n stressSecondary: error.stressSecondary,\n responseActions: error.responseActions\n }\n : (() => {\n const t = classifyStressResponseV0({\n code: code || msg,\n rhizohFailureKind,\n providerHttpStatus: providerHttpMatch\n ? Number(providerHttpMatch[1])\n : null\n });\n return {\n stressClass: t.stressClass,\n responseAction: t.responseAction,\n stressMatrix: t.matrix,\n stressInterpretable: t.interpretable,\n stressConfidence: t.stressConfidence,\n actionConfidence: t.actionConfidence,\n actionSoftened: t.actionSoftened,\n responseActionStrict: t.responseActionStrict,\n actionInterpretable: t.actionInterpretable,\n conflictResolution: t.conflictResolution,\n stressSecondary: t.stressSecondary,\n responseActions: t.responseActions\n };\n })();\n sendJson(res, status, {\n ok: false,\n rhizohFailureKind,\n ...(providerHttpMatch ? { providerHttpStatus: Number(providerHttpMatch[1]) } : {}),\n error: code || msg || \"rhizoh_llm_failed\",\n detail: msg || code || undefined,\n reply:\n error?.reply ||\n (status === 500 ? \"Rhizoh bağlantısı geçici olarak kesildi.\" : msg),\n directive: error?.directive || \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n ...stressTaxonomy\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicSeal) {\n try {\n if (!EPISTEMIC_SEAL_SECRET) {\n sendJson(res, 503, { ok: false, error: \"epistemic_seal_disabled\" });\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`epistemic_seal:${ip}`, RL_EPISTEMIC_SEAL_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const truth = body.truth_contract;\n if (!truth || typeof truth !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"truth_contract_required\" });\n return;\n }\n const sealBody = {\n truth_contract: truth,\n runtime_hash: String(body.runtime_hash || \"\"),\n model_route: {\n provider: body.model_route?.provider ?? null,\n model: body.model_route?.model ?? null\n },\n memory_digest: String(body.memory_digest || \"\"),\n world_snapshot_hash: String(body.world_snapshot_hash || \"\"),\n timestamp: Number(body.timestamp) || Date.now()\n };\n const canonical = canonicalEpistemicSealString(sealBody);\n const { hash, signature } = hashAndSignEpistemicSeal(canonical, EPISTEMIC_SEAL_SECRET);\n recordGenesisEpistemicSealIssued(hash);\n const attestation = {\n schema: EPISTEMIC_SEAL_SCHEMA,\n algorithm: \"SHA-256+HMAC-SHA256\",\n keyHint: process.env.CASTLE_EPISTEMIC_SEAL_SECRET ? \"CASTLE_EPISTEMIC_SEAL_SECRET\" : \"CASTLE_GATEWAY_TOKEN\",\n issuedAt: Date.now(),\n gateway: { service: \"castle-gateway\", listenPort: PORT }\n };\n sendJson(res, 200, {\n ok: true,\n hash,\n signature,\n attestation,\n canonicalBytes: Buffer.byteLength(canonical, \"utf8\")\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"seal_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicLogsBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`epistemic_logs:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 120) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const persisted = await persistEpistemicLedgerBatch(auth.uid, entries);\n recordGenesisEpistemicLedgerPersisted(persisted.persisted);\n // Internal cognition layer: forecast stays gateway/ledger-only (no client exposure).\n try {\n await persistEpistemicForecastBatch(auth.uid, persisted.normalized || []);\n } catch {\n /* forecast failures must not break primary ledger ingest */\n }\n sendJson(res, 200, {\n ok: true,\n persisted: persisted.persisted,\n mode: persisted.mode,\n latest: persisted.normalized?.[persisted.normalized.length - 1] || null\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"epistemic_log_ingest_failed\") });\n }\n return;\n }\n\n sendJson(res, 404, { ok: false, error: \"not_found\" });\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\nspiralWssForGenesis = wss;\nconst orchestrator = createOrchestrator();\nconst clientStats = new Map();\nlet broadcasterClientId = null;\nconst spiralState = {\n rooms: [{ id: \"room-default\", name: \"Spiral Default Arena\" }],\n activeByClient: new Map(),\n characters: new Map() // id -> {id,name,role,roomId,source}\n};\n\n/** Castle social rooms — keyed by `castleRoomKey` from {@link WS_MESSAGE.CASTLE_SOCIAL_PULSE}. */\nconst castleSocialRoomState = new Map();\nconst CASTLE_SOCIAL_MEMBER_TTL_MS = 90_000;\nconst CASTLE_SOCIAL_MAX_PULSE_BYTES = 8192;\nconst RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES = 5 * 1024 * 1024;\nconst RHIZOH_VOICE_LIVE_MAX_CHUNKS = 96;\n\n/**\n * Voice Engine v3 live media lane.\n * Client streams MediaRecorder chunks over the existing gateway WS; STOP reuses the\n * gateway ASR runner so orchestration remains the single authority downstream.\n */\nconst rhizohVoiceLiveSessions = new WeakMap();\n\nfunction sendRhizohVoiceLiveEnvelopeV0(socket, type, payload) {\n try {\n if (socket?.readyState === 1) socket.send(JSON.stringify(createEnvelope(type, payload)));\n } catch {\n /* noop */\n }\n}\n\nfunction decodeRhizohVoiceLiveChunkV0(payload = {}) {\n const b64 = String(payload.audioBase64 || \"\").trim();\n if (!b64) return { ok: false, error: \"audio_base64_required\" };\n try {\n const buf = Buffer.from(b64, \"base64\");\n if (!buf.length) return { ok: false, error: \"audio_decode_empty\" };\n return { ok: true, buffer: buf };\n } catch {\n return { ok: false, error: \"audio_base64_invalid\" };\n }\n}\n\nfunction handleRhizohVoiceLiveStartV0(socket, payload = {}) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const sessionId = String(p.sessionId || `live_${Date.now().toString(36)}`).slice(0, 96);\n rhizohVoiceLiveSessions.set(socket, {\n sessionId,\n traceId: String(p.traceId || \"\").slice(0, 128),\n mimeType: String(p.mimeType || \"audio/webm\").slice(0, 80),\n languageCode: String(p.languageCode || \"tr-TR\").slice(0, 32),\n path: String(p.path || \"both\").slice(0, 24),\n chunks: [],\n totalBytes: 0,\n startedAtMs: Date.now()\n });\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_START, {\n ok: true,\n sessionId,\n schema: \"castle.rhizoh.voice_live.gateway_lane.v0\"\n });\n}\n\nfunction handleRhizohVoiceLiveChunkV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const decoded = decodeRhizohVoiceLiveChunkV0(payload);\n if (!decoded.ok) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: decoded.error\n });\n return;\n }\n if (\n session.chunks.length >= RHIZOH_VOICE_LIVE_MAX_CHUNKS ||\n session.totalBytes + decoded.buffer.length > RHIZOH_VOICE_LIVE_MAX_SESSION_BYTES\n ) {\n rhizohVoiceLiveSessions.delete(socket);\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_audio_too_large\"\n });\n return;\n }\n session.chunks.push(decoded.buffer);\n session.totalBytes += decoded.buffer.length;\n}\n\nfunction handleRhizohVoiceLiveStopV0(socket, payload = {}) {\n const session = rhizohVoiceLiveSessions.get(socket);\n rhizohVoiceLiveSessions.delete(socket);\n if (!session) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n error: \"live_session_missing\"\n });\n return;\n }\n const audio = Buffer.concat(session.chunks);\n if (!audio.length) {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"audio_empty\"\n });\n return;\n }\n runRhizohVoiceTranscribeV3({\n path: String(payload?.path || session.path || \"both\"),\n audioBase64: audio.toString(\"base64\"),\n mimeType: String(payload?.mimeType || session.mimeType || \"audio/webm\"),\n languageCode: String(payload?.languageCode || session.languageCode || \"tr-TR\"),\n traceId: String(payload?.traceId || session.traceId || \"\"),\n sessionId: session.sessionId,\n transport: \"gateway_ws_live\"\n })\n .then((result) => {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_FINAL, {\n ...result,\n sessionId: session.sessionId,\n transportPath: \"gateway_ws_live\",\n bytes: audio.length,\n chunkCount: session.chunks.length,\n recordedMs: Math.max(0, Date.now() - session.startedAtMs)\n });\n })\n .catch((e) => {\n sendRhizohVoiceLiveEnvelopeV0(socket, WS_MESSAGE.RHIZOH_VOICE_LIVE_ERROR, {\n ok: false,\n sessionId: session.sessionId,\n error: \"live_transcribe_failed\",\n detail: String(e?.message || e)\n });\n });\n}\n\nfunction removeCastleSocialClientFromAllRooms(clientId) {\n const cid = String(clientId || \"\");\n if (!cid) return;\n for (const room of castleSocialRoomState.values()) {\n room.members.delete(cid);\n }\n}\n\nfunction buildCastleSocialRoomPayload(roomKey) {\n const room = castleSocialRoomState.get(roomKey);\n if (!room) return null;\n const now = Date.now();\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const roster = [...room.members.values()].map((m) => ({\n userId: m.userId,\n lastMs: m.lastMs,\n ...m.sp\n }));\n const peerFeeds =\n room.walPeers && room.walPeers.size\n ? [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }))\n : [];\n return { castleRoomKey: roomKey, seq: room.seq, roster, peerFeeds, ts: now };\n}\n\nconst CASTLE_WAL_PEER_MAX_FEED_BYTES = 48 * 1024;\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {{ ok: boolean, code?: string, reason?: string }} gate\n */\nfunction rejectWalPeerIngressV0(socket, gate) {\n const code = gate.code || \"wal_peer_rejected\";\n recordWalPeerRejectV0(code);\n recordSubstrateOtelEventV0(\"wal_peer_reject\", { code, reason: String(gate.reason || \"\").slice(0, 120) });\n socket.send(\n JSON.stringify(\n createEnvelope(WS_MESSAGE.ERROR, {\n error: gate.reason || \"WAL peer ingress rejected.\",\n code\n })\n )\n );\n}\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {Record<string, unknown> | null | undefined} walPeerFeed\n * @returns {boolean}\n */\nfunction gateWalPeerIngressV0(socket, walPeerFeed) {\n const authGate = assertWalPeerSocketAuthorityV0(socket);\n if (!authGate.ok) {\n rejectWalPeerIngressV0(socket, authGate);\n return false;\n }\n const sigGate = validateWalPeerFeedSignatureV0(\n walPeerFeed && typeof walPeerFeed === \"object\" ? walPeerFeed : {}\n );\n if (!sigGate.ok) {\n rejectWalPeerIngressV0(socket, sigGate);\n return false;\n }\n return true;\n}\n\nfunction handleCastleWalPeerFeed(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_WAL_PEER_MAX_FEED_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_WAL_PEER_FEED too large.\" })));\n return;\n }\n const walPeerFeed = p.walPeerFeed && typeof p.walPeerFeed === \"object\" ? p.walPeerFeed : {};\n if (!gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const castleRoomKey = String(p.castleRoomKey || \"wal:convergence\").slice(0, 64);\n const castleId = String(p.castleId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId,\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: room.members.get(socket.clientId)?.sp ?? {}\n });\n const peerFeeds = [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }));\n const body = {\n castleRoomKey,\n seq: room.seq,\n peerFeeds,\n ts: now\n };\n const env = createEnvelope(WS_MESSAGE.CASTLE_WAL_PEER_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys(), ...room.walPeers.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction handleCastleSocialPulse(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_SOCIAL_MAX_PULSE_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_SOCIAL_PULSE too large.\" })));\n return;\n }\n const castleRoomKey = String(p.castleRoomKey || \"default\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n const sp = p.socialPulse && typeof p.socialPulse === \"object\" ? p.socialPulse : {};\n const walPeerFeed =\n p.walPeerFeed && typeof p.walPeerFeed === \"object\"\n ? p.walPeerFeed\n : sp.walPeerFeed && typeof sp.walPeerFeed === \"object\"\n ? sp.walPeerFeed\n : null;\n if (walPeerFeed && !gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const spNext = walPeerFeed ? { ...sp, walPeerFeed } : { ...sp };\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: spNext\n });\n if (walPeerFeed) {\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId: String(p.castleId || userId).slice(0, 64),\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n }\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const body = buildCastleSocialRoomPayload(castleRoomKey);\n if (!body) return;\n const env = createEnvelope(WS_MESSAGE.CASTLE_SOCIAL_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcast(msgObj) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcastState() {\n broadcast(\n createEnvelope(WS_MESSAGE.BROADCAST_STATE, {\n broadcasterClientId,\n viewers: [...wss.clients].filter((c) => c.readyState === 1).length\n })\n );\n}\n\nfunction currentSpiralStatePayload() {\n return {\n rooms: spiralState.rooms,\n characters: [...spiralState.characters.values()]\n };\n}\n\nfunction broadcastSpiralState() {\n broadcast(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload()));\n}\n\nfunction ensureRoom(roomId, fallbackName = null) {\n if (!roomId) return spiralState.rooms[0].id;\n const exists = spiralState.rooms.find((r) => r.id === roomId);\n if (exists) return exists.id;\n const created = { id: roomId, name: fallbackName || `Spiral-${roomId.slice(-4)}` };\n spiralState.rooms.push(created);\n return created.id;\n}\n\nfunction syncSpiralCharactersWithWorld(snapshot) {\n const agents = snapshot?.agents || [];\n for (const a of agents) {\n if (!spiralState.characters.has(a.id)) {\n spiralState.characters.set(a.id, {\n id: a.id,\n name: a.id,\n role: \"agent\",\n roomId: spiralState.rooms[0].id,\n source: \"world\"\n });\n }\n }\n}\n\nfunction broadcastExcept(msgObj, exceptSocket) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client !== exceptSocket && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction validateInputFrame(payload, socket) {\n const stats = clientStats.get(socket.clientId) || { lastFrame: 0, windowStart: Date.now(), msgCount: 0 };\n const now = Date.now();\n if (now - stats.windowStart > 5000) {\n stats.windowStart = now;\n stats.msgCount = 0;\n }\n stats.msgCount += 1;\n clientStats.set(socket.clientId, stats);\n if (stats.msgCount > 120) return \"Rate limit exceeded.\";\n\n if (!payload || !Number.isInteger(payload.frame)) return \"frame must be integer.\";\n if (payload.frame <= stats.lastFrame) return \"frame must be monotonic.\";\n const commands = Array.isArray(payload.commands) ? payload.commands : [];\n if (commands.length > 8) return \"Too many commands in one frame.\";\n for (const cmd of commands) {\n if (!cmd || typeof cmd.command !== \"string\") return \"Invalid command format.\";\n if (\n cmd.command !== \"SPAWN_AGENT\" &&\n cmd.command !== \"LIST_CASTLES\" &&\n cmd.command !== \"SPAWN_ENTITY\" &&\n cmd.command !== \"CREATE_CASTLE\"\n ) return \"Unsupported command.\";\n }\n stats.lastFrame = payload.frame;\n clientStats.set(socket.clientId, stats);\n return null;\n}\n\nfunction validateSignalPayload(payload) {\n if (!payload || typeof payload !== \"object\") return \"Invalid signaling payload.\";\n const signalType = payload.signalType;\n if (![\"OFFER\", \"ANSWER\", \"ICE\"].includes(signalType)) return \"Invalid signal type.\";\n if (typeof payload.to !== \"string\" || payload.to.length < 3) return \"Invalid signal destination.\";\n if (signalType !== \"ICE\" && JSON.stringify(payload.sdp || {}).length > 16 * 1024) return \"SDP too large.\";\n if (signalType === \"ICE\" && JSON.stringify(payload.candidate || {}).length > 6 * 1024) return \"ICE too large.\";\n return null;\n}\n\nfunction sanitizeRawMessage(raw) {\n if (!raw) return null;\n const text = raw.toString();\n if (Buffer.byteLength(text, \"utf8\") > MAX_MESSAGE_BYTES) return null;\n return text;\n}\n\nwss.on(\"connection\", async (socket, req) => {\n const origin = req?.headers?.origin || \"\";\n if (!httpCors.isWebSocketOriginAllowed(origin)) {\n socket.close(1008, \"Origin not allowed\");\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const path = req?.url || \"\";\n const query = new URL(`ws://localhost${path}`).searchParams;\n const token = query.get(\"token\");\n if (!token || token !== REQUIRED_GATEWAY_TOKEN) {\n socket.close(1008, \"Invalid gateway token\");\n return;\n }\n }\n if (REQUIRE_AUTH) {\n const result = await verifyClientToken(req);\n if (!result.ok) {\n const isLocal = origin.includes(\"localhost\") || origin.includes(\"127.0.0.1\") || !origin;\n if (!(ALLOW_DEV_ANON && isLocal)) {\n socket.close(1008, result.reason);\n return;\n }\n socket.auth = { ok: true, kind: \"dev-anon\", user: { uid: \"dev-anon\" } };\n } else {\n socket.auth = result;\n }\n }\n\n socket.clientId = `c-${Math.random().toString(36).slice(2, 8)}`;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.HELLO, { message: \"Castle Gateway online.\", clientId: socket.clientId })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.WORLD_SNAPSHOT, orchestrator.worldSnapshot())));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.BROADCAST_STATE, { broadcasterClientId, viewers: [...wss.clients].filter((c) => c.readyState === 1).length })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload())));\n broadcastExcept(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }), socket);\n\n socket.on(\"message\", (raw) => {\n const safeRaw = sanitizeRawMessage(raw);\n if (!safeRaw) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Message too large or invalid.\" })));\n return;\n }\n const parsed = safeJsonParse(safeRaw);\n if (!parsed?.type) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid message.\" })));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND_TEXT) {\n const canonical = parseTextToCommand(parsed.payload?.text);\n if (!canonical) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Unknown command text.\" })));\n return;\n }\n const result = orchestrator.applyCommand(canonical);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND) {\n const result = orchestrator.applyCommand(parsed.payload);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n }\n\n if (parsed.type === WS_MESSAGE.INPUT_FRAME) {\n const err = validateInputFrame(parsed.payload, socket);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const frame = parsed.payload.frame;\n const commands = Array.isArray(parsed.payload?.commands) ? parsed.payload.commands : [];\n orchestrator.queueInputFrame(socket.clientId, frame, commands);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, queued: true, frame })));\n }\n\n if (parsed.type === WS_MESSAGE.SIGNAL) {\n const payload = parsed.payload || {};\n const err = validateSignalPayload(payload);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const signalType = payload.signalType;\n const to = payload.to;\n for (const client of wss.clients) {\n if (client.clientId === to && client.readyState === 1) {\n client.send(JSON.stringify(createEnvelope(WS_MESSAGE.SIGNAL, { ...payload, from: socket.clientId })));\n break;\n }\n }\n }\n\n if (parsed.type === WS_MESSAGE.DRONE_TELEMETRY) {\n const payload = parsed.payload || {};\n const json = JSON.stringify(payload);\n if (Buffer.byteLength(json, \"utf8\") > 48 * 1024) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"DRONE_TELEMETRY payload too large.\" })));\n return;\n }\n broadcastExcept(createEnvelope(WS_MESSAGE.DRONE_TELEMETRY, { ...payload, relayFrom: socket.clientId }), socket);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_START) {\n handleRhizohVoiceLiveStartV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_CHUNK) {\n handleRhizohVoiceLiveChunkV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_VOICE_LIVE_STOP) {\n handleRhizohVoiceLiveStopV0(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_SOCIAL_PULSE) {\n handleCastleSocialPulse(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_WAL_PEER_FEED) {\n handleCastleWalPeerFeed(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.OPEN_DATA_QUERY) {\n queryOpenData(parsed.payload)\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: true, result })));\n })\n .catch((e) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: false, error: e.message })));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_QUERY) {\n runRhizohBrainV2({\n ...(parsed.payload || {}),\n uid: socket?.auth?.user?.uid || \"unknown\"\n })\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, result)));\n })\n .catch(async () => {\n const fallback = await runRhizohBrain(parsed.payload || {}).catch(() => ({\n ok: false,\n reply: \"Rhizoh temporary disturbance. Habitat remains stable.\"\n }));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, fallback)));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.BROADCAST_REGISTER) {\n const role = String(parsed.payload?.role || \"\");\n if (role === \"GENESIS_BROADCAST_AGENT\") {\n broadcasterClientId = socket.clientId;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, registered: role })));\n broadcastState();\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.STUDIO_CUE) {\n // Only registered broadcaster can push global cues.\n if (!broadcasterClientId || socket.clientId !== broadcasterClientId) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Only broadcaster agent can emit STUDIO_CUE.\" })));\n return;\n }\n const cue = parsed.payload || {};\n if (typeof cue !== \"object\") {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid STUDIO_CUE payload.\" })));\n return;\n }\n broadcast(createEnvelope(WS_MESSAGE.STUDIO_CUE, cue));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_JOIN_ROOM) {\n const requested = String(parsed.payload?.roomId || \"\").trim();\n const requestedName = String(parsed.payload?.roomName || \"\").trim();\n const roomId = ensureRoom(requested || `room-${Math.random().toString(36).slice(2, 7)}`, requestedName || null);\n spiralState.activeByClient.set(socket.clientId, roomId);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, roomId })));\n broadcastSpiralState();\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_CREATE_CHARACTER) {\n const name = String(parsed.payload?.name || \"\").trim().slice(0, 32) || `AI-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;\n const role = String(parsed.payload?.role || \"agent\").trim().slice(0, 24) || \"agent\";\n const roomId = ensureRoom(\n String(parsed.payload?.roomId || spiralState.activeByClient.get(socket.clientId) || spiralState.rooms[0].id),\n null\n );\n\n // Character creation always maps to an authoritative world agent.\n const result = orchestrator.applyCommand({\n command: COMMAND.SPAWN_AGENT,\n params: { targetCastleId: \"castle-01\" }\n });\n if (!result?.ok || !result?.data?.id) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Character creation failed.\" })));\n return;\n }\n const id = result.data.id;\n spiralState.characters.set(id, { id, name, role, roomId, source: \"spawned\" });\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, characterId: id, roomId })));\n broadcastSpiralState();\n return;\n }\n });\n\n socket.on(\"close\", () => {\n if (socket.clientId === broadcasterClientId) broadcasterClientId = null;\n clientStats.delete(socket.clientId);\n rhizohVoiceLiveSessions.delete(socket);\n spiralState.activeByClient.delete(socket.clientId);\n removeCastleSocialClientFromAllRooms(socket.clientId);\n broadcast(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }));\n broadcastState();\n });\n});\n\nsetInterval(() => {\n void (async () => {\n try {\n const { frame, snapshot, delta, packedDelta } = orchestrator.tick(1 / 20);\n syncSpiralCharactersWithWorld(snapshot);\n broadcast(createEnvelope(WS_MESSAGE.NETWORK_TICK, { frame }));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_TICK, snapshot));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA, delta));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA_PACKED, packedDelta));\n broadcastSpiralState();\n await runAcademyEventTick();\n const executed = await tickAutomations(Date.now());\n for (const a of executed) {\n await addTranscript(a.uid, {\n source: \"planner\",\n eventType: \"automation-executed\",\n text: `Automation executed: ${a.title} (${a.action})`,\n roomId: \"studio-main\",\n meta: { automationId: a.id, action: a.action, deviceId: a.deviceId || \"\" }\n });\n }\n } catch (e) {\n console.error(\"[GATEWAY] world tick / automations loop error:\", String(e?.message || e));\n }\n })();\n}, 50);\n\n(async () => {\n await rhizohRuntime.telemetry.initOpenTelemetry();\n if (String(process.env.CASTLE_GENESIS_DISK_PERSIST ?? \"\").trim() === \"\") {\n process.env.CASTLE_GENESIS_DISK_PERSIST = \"1\";\n }\n const genesisDiskBoot = await hydrateGenesisContinuityPersistenceBootV0();\n if (!genesisDiskBoot.ok) {\n console.warn(\"[GATEWAY] genesis disk hydrate failed; continuing with in-memory genesis continuity\");\n }\n httpServer.listen(PORT, () => {\n startGenesisCanonicalClock();\n installGenesisCheckpointSurfaceGetter(buildGenesisRuntimeSurfacePayloadLive);\n setGenesisContinuityAfterPublishHook((seq) => {\n noteGenesisCheckpointSeqCommitted(seq);\n noteGenesisContinuitySeqAcceptedV0(seq);\n });\n startGenesisContinuityInfraSampler(buildGenesisRuntimeSurfacePayloadLive, 2000);\n logProductionObservatorySurfaceGuardsV0();\n logGatewaySubstrateAuthorityGuardsV0();\n console.log(`[GATEWAY] ws/http://localhost:${PORT}`);\n console.log(\n `[GATEWAY] genesis continuity: GET ${rhizohRuntime.routes.genesisRuntime} | POST ${rhizohRuntime.routes.genesisIngress} | SSE ${rhizohRuntime.routes.genesisStream} | replay ${rhizohRuntime.routes.genesisReplay} | diff ${rhizohRuntime.routes.genesisReplayDiff} | equiv ${rhizohRuntime.routes.genesisReplayEquivalence} | analytics ${rhizohRuntime.routes.genesisReplayAnalytics} | evolution ${rhizohRuntime.routes.genesisReplayEvolution}`\n );\n if (String(process.env.CASTLE_GENESIS_OBSERVATORY_LIVE_V01 ?? \"\").trim() === \"1\") {\n console.log(\"GENESIS OBSERVATORY LIVE v0.1 — READ ONLY PHASE STARTED\");\n }\n });\n})().catch((e) => {\n console.error(\"[GATEWAY] fatal boot (before or during listen):\", e);\n process.exit(1);\n});\n"}, "3b9626bb622a27226597f35b9810e80212f37987": {"t": 1785754156.758944, "n": 156609, "h": "14597890d8235ecf", "c": "import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport dotenv from \"dotenv\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconsole.log(\"🔥 [GENESIS_BOOT] entry file:\", __filename);\nconsole.log(\"🔥 [GENESIS_BOOT] node env:\", process.env.NODE_ENV);\nconsole.log(\"🔥 [GENESIS_BOOT] pid:\", process.pid);\nconsole.log(\"🧬 [GENESIS_BOOT] cwd:\", process.cwd());\nconsole.log(\"🧬 [GENESIS_BOOT] import.meta.url:\", import.meta.url);\ndotenv.config({ path: path.join(__dirname, \"..\", \".env\") });\ndotenv.config({ path: path.join(__dirname, \"..\", \".env.local\"), override: true });\n\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport { createServer } from \"node:http\";\nimport { createOrchestrator } from \"@castle/orchestrator\";\nimport { parseTextToCommand } from \"@castle/command-dsl\";\nimport { WS_MESSAGE, COMMAND, createEnvelope, safeJsonParse } from \"@castle/protocol\";\nimport { queryOpenData } from \"./openData.js\";\nimport { verifyClientToken } from \"./auth.js\";\nimport { runRhizohBrain } from \"./rhizohBrain.js\";\nimport { queryRhizohLlm } from \"./rhizohLlmGateway.js\";\nimport { rhizohGatewayTurn } from \"./rhizohGatewayTurn.js\";\nimport {\n RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n runRhizohVoiceTranscribeV3,\n rhizohVoiceTranscribeEnvV3\n} from \"./rhizohVoiceTranscribeV3.js\";\nimport { createHttpCorsPolicy, normalizeHttpOrigin, parseAllowedOriginsFromEnv } from \"./httpCorsPolicyV1.js\";\nimport {\n applyLanguagePropagationToLlmPayloadV1,\n applyLanguagePropagationToVoiceBodyV1,\n buildRhizohLanguagePropagationEchoV1,\n parseRhizohLanguagePropagationV1\n} from \"./rhizohLanguagePropagationV1.js\";\nimport { trimRhizohLlmGatewayPayloadV1 } from \"./rhizohLlmPayloadTrimV1.js\";\nimport {\n meshAppendDelta,\n meshContinuityAggregate,\n meshJoin,\n meshLeave,\n meshReplayFromNodeId,\n meshReplayFromSeq,\n meshSnapshot,\n meshSubscribeSse\n} from \"./presenceRoomMesh.js\";\nimport { runRhizohBrainV2 } from \"./rhizohBrainV2.js\";\nimport {\n listConnections,\n createConnection,\n updateConnection,\n deleteConnection,\n setDefaultConnection,\n resolveConnection\n} from \"./llmConnectionsStore.js\";\nimport { checkHttpRateLimit, getHttpClientIp } from \"./castleHttpRateLimit.js\";\nimport {\n submitAbuseReportV0,\n listModerationQueueV0,\n softBlockUserV0,\n removeSoftBlockV0,\n listSoftBlockedUsersV0\n} from \"./ops/moderationMvpV0.js\";\nimport { listRecentAgentSnapshotsV0 } from \"./ops/agentObservabilityV0.js\";\nimport { readAgentContainmentConfigV0 } from \"./ops/agentContainmentV0.js\";\nimport { readCostContainmentConfigV0, getCostContainmentStatsV0 } from \"./ops/costContainmentV0.js\";\nimport { getGlobalCostLedgerSnapshotV0, getGclBackendStatusV0 } from \"./ops/globalCostLedgerV0.js\";\nimport { evaluateEconomicStrategyL3V0 } from \"./ops/economicStrategyEngineL3V0.js\";\nimport { getPhasedRolloutStatsV0, readPhasedRolloutTierV0 } from \"./ops/phasedRolloutV0.js\";\nimport { buildUnifiedStateNarrativeV0 } from \"./ops/unifiedStateNarrativeV0.js\";\nimport { resolveHardeningTenantIdV0 } from \"./ops/narrativeTenantIsolationV0.js\";\nimport {\n classifyStressResponseV0,\n verifyStressTaxonomyCoverageV0,\n verifyStressConflictResolutionV0\n} from \"./ops/stressResponseTaxonomyV0.js\";\nimport { verifyResolutionStabilityEnvelopeV0 } from \"./ops/resolutionStabilityEnvelopeV0.js\";\nimport { logLlmAccess } from \"./castleLlmAudit.js\";\nimport { appendMemory, listMemories, getMemoryContext, getPersonaGoalMemory, setPersonaGoalMemory, autoCompactMemories } from \"./memoryStore.js\";\nimport { getFirebasePersistence } from \"./firebasePersistence.js\";\nimport { registerAgentIdentity, listAgentIdentities, getAgentIdentity, updateAgentIdentity } from \"./agentIdentityStore.js\";\nimport { queueAcademyEvent, listAcademyEvents, resolveAcademyEvent, runAcademyEventTick } from \"./academyEventEngine.js\";\nimport { enqueuePdfJob, getPdfJob } from \"./pdfPipeline.js\";\nimport {\n createPublishSession,\n listPublishSessions,\n updatePublishSession,\n addTranscript,\n listTranscripts,\n searchMetadata,\n registerDevice,\n listDevices,\n updateDevice,\n appendTelemetry,\n listTelemetry,\n createAutomation,\n listAutomations,\n tickAutomations,\n enqueueCommand,\n listCommands,\n updateCommandStatus,\n registerSocialChannel,\n listSocialChannels\n} from \"./studioOpsStore.js\";\nimport { metrics as infraMetrics } from \"./infra/metrics.js\";\nimport { renderRhizohPrometheusMetrics, rhizohEnterpriseMetrics } from \"./infra/rhizohEnterpriseMetrics.js\";\nimport { scoreHealth } from \"./infra/healthScore.js\";\nimport { getTraceById, listTracesBySession } from \"./infra/traceRegistry.js\";\nimport { canonicalEpistemicSealString, hashAndSignEpistemicSeal, EPISTEMIC_SEAL_SCHEMA } from \"./epistemicSeal.js\";\nimport { persistEpistemicLedgerBatch } from \"./epistemicLedgerStore.js\";\nimport { persistEpistemicForecastBatch } from \"./epistemicForecastStore.js\";\nimport { buildRhizohExternalGroundTruthPayload } from \"./rhizohExternalGroundTruthGateway.js\";\nimport { ingestRhizohExternalLossBatchHttp } from \"./rhizohExternalLossIngestGateway.js\";\nimport {\n ingestRhizohProductOutcomeHttp,\n listRhizohProductOutcomeAggregates,\n verifyRhizohOutcomeHmac,\n verifyRhizohOutcomeSourceToken\n} from \"./rhizohProductOutcomeIngestGateway.js\";\nimport { initRhizoh } from \"./rhizohProductionBootstrap.js\";\nimport { handleAcademicObservatoryExportGetV0 } from \"./rhizoh/academicObservatoryHttpV0.js\";\nimport {\n handleLiveSportsBundleGetV0,\n handleLiveNewsHeadlinesGetV0,\n handleLiveWorldFeedGetV0\n} from \"./rhizoh/liveWorldFeedHttpV0.js\";\nimport {\n buildGenesisRuntimeSurfacePayload,\n recordGenesisEpistemicLedgerPersisted,\n recordGenesisEpistemicSealIssued,\n startGenesisCanonicalClock\n} from \"./genesisRuntimeSurfaceV0.js\";\nimport { registerGenesisSseClient, getGenesisContinuitySeq, setGenesisContinuityAfterPublishHook } from \"./genesisContinuityStreamHubV0.js\";\nimport { computeGenesisReplayFingerprintV0 } from \"./genesisReplayFingerprintV0.js\";\nimport { buildGenesisGatewayCapabilitiesV0 } from \"./genesisGatewayCapabilitiesV0.js\";\nimport {\n installGenesisCheckpointSurfaceGetter,\n noteGenesisCheckpointSeqCommitted,\n getLatestGenesisSignedCheckpoint\n} from \"./genesisContinuityCheckpointV0.js\";\nimport { hydrateGenesisContinuityPersistenceBootV0 } from \"./genesisContinuityHydrateBootV0.js\";\nimport { startGenesisContinuityInfraSampler } from \"./genesisContinuityInfraSamplerV0.js\";\nimport {\n genesisCheckpointQueryBySeqV0,\n genesisCheckpointQueryRangeV0,\n genesisCheckpointQueryLineageV0\n} from \"./genesisCheckpointQueryV0.js\";\nimport {\n genesisContinuityEventArchiveEnabled,\n queryGenesisContinuityEventArchiveV0,\n GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION\n} from \"./genesisContinuityEventArchiveV0.js\";\nimport { resolveGenesisReplayRouterV1, GENESIS_REPLAY_ROUTER_SCHEMA } from \"./genesisReplayRouterV1.js\";\nimport {\n computeGenesisReplayTemporalDiffV1,\n GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA\n} from \"./genesisReplayTemporalDiffV1.js\";\nimport { compareGenesisReplayEquivalenceV1 } from \"./genesisReplayEquivalenceV1.js\";\nimport { computeGenesisReplayAnalyticsV1 } from \"./genesisReplayAnalyticsV1.js\";\nimport { computeGenesisReplayEvolutionV1 } from \"./genesisReplayEvolutionV1.js\";\nimport { handleStripeCheckoutCreate, handleStripeMembershipWebhook } from \"./stripeWebhookMembership.js\";\nimport { buildRealityHealthPayload } from \"./edgeRealityConsistencyV1.js\";\nimport {\n assertWalPeerSocketAuthorityV0,\n buildGatewaySubstrateAuthoritySnapshotV0,\n logGatewaySubstrateAuthorityGuardsV0,\n validateWalPeerFeedSignatureV0\n} from \"./gatewaySubstrateAuthorityV0.js\";\nimport {\n buildSubstrateOperationalSnapshotV0,\n recordWalPeerRejectV0,\n renderSubstratePrometheusMetricsV0\n} from \"./infra/substrateOperationalMetrics.js\";\nimport { ingestClientSubstrateHealthV0 } from \"./rhizohSubstrateHealthIngestV0.js\";\nimport { recordSubstrateOtelEventV0 } from \"./infra/opentelemetrySubstrateV0.js\";\nimport {\n GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n getGenesisWorldObservationIngressStatusV0,\n ingestGenesisWorldObservationV0\n} from \"./genesisContinuityClientIngressV0.js\";\nimport {\n getGenesisContinuitySeqAuditV0,\n noteGenesisContinuitySeqAcceptedV0\n} from \"./genesisContinuitySeqGapV0.js\";\n\n/** Render / Fly / Railway: `PORT` — yoksa `CASTLE_GATEWAY_PORT` — yoksa 8090. */\nconst PORT =\n Number(process.env.PORT) ||\n Number(process.env.CASTLE_GATEWAY_PORT) ||\n 8090;\n/** Spiral / world WebSocket server — set after `wss` init; used by genesis runtime surface. */\nlet spiralWssForGenesis = null;\nconst MAX_MESSAGE_BYTES = Number(process.env.CASTLE_MAX_MESSAGE_BYTES || 32 * 1024);\nconst REQUIRED_GATEWAY_TOKEN = process.env.CASTLE_GATEWAY_TOKEN || \"\";\nconst ALLOWED_ORIGINS = parseAllowedOriginsFromEnv().fromList;\nconst httpCors = createHttpCorsPolicy();\n\n/** @returns {string|null} `Access-Control-Allow-Origin`; null = browser CORS fail. */\nfunction accessControlAllowOriginValue(req) {\n return httpCors.accessControlAllowOriginValue(req);\n}\n\nfunction applyHttpCorsHeaders(req, res) {\n return httpCors.applyHttpCorsHeaders(req, res);\n}\nconst REQUIRE_AUTH = process.env.CASTLE_REQUIRE_AUTH === \"true\";\nconst ALLOW_DEV_ANON = process.env.CASTLE_ALLOW_DEV_ANON !== \"false\";\nconst ALLOW_DEV_HTTP_UID = process.env.CASTLE_ALLOW_DEV_HTTP_UID !== \"false\";\n/** Presence mesh: allow `X-Castle-Guest-Id` when Firebase auth absent (default on). Set `false` to require auth only. */\nconst ALLOW_MESH_GUEST = process.env.CASTLE_ALLOW_MESH_GUEST !== \"false\";\nconst SOCIAL_RETRY_MAX = Math.max(0, Math.min(5, Number(process.env.CASTLE_SOCIAL_RETRY_MAX || 2)));\nconst SOCIAL_RETRY_BASE_MS = Math.max(100, Number(process.env.CASTLE_SOCIAL_RETRY_BASE_MS || 700));\nconst TELEGRAM_BOT_TOKEN = process.env.CASTLE_TELEGRAM_BOT_TOKEN || \"\";\nconst WHATSAPP_TOKEN = process.env.CASTLE_WHATSAPP_TOKEN || \"\";\nconst WHATSAPP_PHONE_NUMBER_ID = process.env.CASTLE_WHATSAPP_PHONE_NUMBER_ID || \"\";\nconst RL_RHIZOH_LLM_PER_MIN = Math.max(5, Number(process.env.CASTLE_RL_RHIZOH_LLM_PER_MIN || 40));\nconst RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN || 30)\n);\nconst RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN || 120));\nconst RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN = Math.max(\n 10,\n Number(process.env.CASTLE_RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN || 60)\n);\nconst RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN = Math.max(8, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN || 48));\nconst RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN = Math.max(4, Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN || 24));\nconst RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN || 18)\n);\nconst RL_EPISTEMIC_SEAL_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_EPISTEMIC_SEAL_PER_MIN || 120));\nconst RL_EPISTEMIC_LOG_PER_MIN = Math.max(20, Number(process.env.CASTLE_RL_EPISTEMIC_LOG_PER_MIN || 240));\nconst EPISTEMIC_SEAL_SECRET = String(\n process.env.CASTLE_EPISTEMIC_SEAL_SECRET || process.env.CASTLE_GATEWAY_TOKEN || \"\"\n).trim();\nconst RL_LLM_CONN_TEST_PER_MIN = Math.max(2, Number(process.env.CASTLE_RL_LLM_CONNECTION_TEST_PER_MIN || 8));\n\n/** One-shot production warnings: CORS surface, auth toggle, dev bypass flags (observatory deploy hygiene). */\nfunction logProductionObservatorySurfaceGuardsV0() {\n if (String(process.env.NODE_ENV || \"\").toLowerCase() !== \"production\") return;\n const tag = \"[GATEWAY_PROD_GUARD]\";\n const primaryCors = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n const corsListEmpty = ALLOWED_ORIGINS.length === 0;\n if (corsListEmpty && !primaryCors) {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty and CASTLE_HTTP_CORS_ORIGIN empty — production still merges RHIZOH_BROWSER_ORIGINS_V1 defaults; set env for preview/staging hosts`\n );\n } else if (corsListEmpty && primaryCors && primaryCors !== \"*\") {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty — only CASTLE_HTTP_CORS_ORIGIN set; add web.app + firebaseapp.com (and previews) to CASTLE_ALLOWED_ORIGINS if clients use alternate hostnames`\n );\n }\n if (primaryCors && /localhost|127\\.0\\.0\\.1/i.test(primaryCors)) {\n console.warn(`${tag} CORS: CASTLE_HTTP_CORS_ORIGIN is localhost while NODE_ENV=production`);\n }\n if (REQUIRE_AUTH) {\n console.warn(\n `${tag} AUTH: CASTLE_REQUIRE_AUTH=true — clients must send a valid gateway credential (otherwise silent 401 on replay/SSE); EventSource cannot set custom headers unless proxied`\n );\n if (REQUIRED_GATEWAY_TOKEN.length < 16) {\n console.warn(`${tag} AUTH: CASTLE_GATEWAY_TOKEN missing or short while require-auth enabled`);\n }\n }\n if (ALLOW_DEV_ANON) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_ANON is not \"false\" — dev anonymous path may be live in production (set CASTLE_ALLOW_DEV_ANON=false)`\n );\n }\n if (ALLOW_DEV_HTTP_UID) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_HTTP_UID is not \"false\" — X-Castle-Dev-Uid bypass risk (set CASTLE_ALLOW_DEV_HTTP_UID=false)`\n );\n }\n}\n\nconst rhizohRuntime = initRhizoh();\n\n/** İstemci: llmKeySource veya keySource — env | user_connection | auto */\nfunction normalizeClientLlmKeySource(raw) {\n const s = String(raw ?? \"\").trim().toLowerCase();\n if (s === \"env\" || s === \"server\") return \"env\";\n if (s === \"user_connection\" || s === \"connection\" || s === \"user\") return \"user_connection\";\n if (s === \"auto\" || s === \"\") return \"auto\";\n return null;\n}\n\nfunction sendJson(res, statusCode, payload) {\n res.writeHead(statusCode, { \"Content-Type\": \"application/json; charset=utf-8\" });\n res.end(JSON.stringify(payload));\n}\n\nfunction logRhizohHealth(stage, detail = {}) {\n try {\n console.info(`[CASTLE_${String(stage || \"unknown\")}]`, detail && typeof detail === \"object\" ? detail : {});\n } catch {\n /* noop */\n }\n}\n\nfunction renderInfraMetricsProm() {\n const lines = [\n `castle_gateway_events_processed_total ${infraMetrics.eventsProcessed}`,\n `castle_gateway_duplicate_reject_total ${infraMetrics.duplicateRejects}`,\n `castle_gateway_enqueue_latency_ms ${infraMetrics.enqueueLatencyMs}`,\n `castle_gateway_queue_depth ${infraMetrics.queueDepth}`,\n `castle_gateway_queue_lag_seconds ${(Number(infraMetrics.queueLag || 0) / 1000).toFixed(3)}`,\n `castle_gateway_errors_total ${infraMetrics.errors}`\n ];\n const buckets = infraMetrics.enqueueLatencyBuckets || {};\n for (const k of Object.keys(buckets)) {\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"${k}\"} ${buckets[k]}`);\n }\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"+Inf\"} ${infraMetrics.eventsProcessed}`);\n if (process.env.CASTLE_SUBSTRATE_METRICS !== \"0\") {\n lines.push(renderSubstratePrometheusMetricsV0().trimEnd());\n }\n return lines.join(\"\\n\");\n}\n\nasync function fetchJsonWithTimeout(url, timeoutMs = 1200) {\n const ctl = new AbortController();\n const tid = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const res = await fetch(url, { signal: ctl.signal });\n if (!res.ok) return null;\n return await res.json();\n } catch {\n return null;\n } finally {\n clearTimeout(tid);\n }\n}\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction getHttpPathname(req) {\n let p = String(req.url || \"\").split(\"?\")[0];\n /* Reverse proxies / clients sometimes append `/` — avoid silent 404 on e.g. `/rhizoh/genesis/stream/`. */\n while (p.length > 1 && p.endsWith(\"/\")) {\n p = p.slice(0, -1);\n }\n return p;\n}\n\nfunction rhizohLlmEnvConfigured() {\n const p = String(process.env.CASTLE_LLM_PROVIDER || \"openai\").toLowerCase();\n const key =\n p === \"anthropic\"\n ? process.env.ANTHROPIC_API_KEY\n : p === \"gemini\"\n ? process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY\n : p === \"xai\"\n ? process.env.XAI_API_KEY\n : p === \"deepseek\"\n ? process.env.DEEPSEEK_API_KEY\n : p === \"mistral\"\n ? process.env.MISTRAL_API_KEY\n : p === \"openrouter\"\n ? process.env.OPENROUTER_API_KEY\n : process.env.OPENAI_API_KEY;\n return !!String(key || \"\").trim();\n}\n\n/**\n * @param {import(\"firebase-admin/firestore\").Firestore | null} db\n * @param {number} timeoutMs\n */\nasync function probeFirestoreReachable(db, timeoutMs = 2000) {\n if (!db) return { ok: false, reason: \"no_db\" };\n const timeout = sleep(timeoutMs).then(() => ({ ok: false, reason: \"timeout\" }));\n const work = db\n .collection(\"_castle_gateway_health\")\n .doc(\"ping\")\n .get()\n .then(() => ({ ok: true, reason: \"reachable\" }))\n .catch((e) => ({ ok: false, reason: String(e?.message || e || \"firestore_error\") }));\n return Promise.race([work, timeout]);\n}\n\nfunction stripHtml(text = \"\") {\n return String(text)\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \" \")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \" \")\n .replace(/<[^>]+>/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction haversineMeters(aLat, aLon, bLat, bLon) {\n const R = 6371000;\n const dLat = ((bLat - aLat) * Math.PI) / 180;\n const dLon = ((bLon - aLon) * Math.PI) / 180;\n const sa = Math.sin(dLat / 2);\n const sb = Math.sin(dLon / 2);\n const x = sa * sa + Math.cos((aLat * Math.PI) / 180) * Math.cos((bLat * Math.PI) / 180) * sb * sb;\n return 2 * R * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));\n}\n\nasync function checkPublishTargetHealth(protocol, target) {\n const p = String(protocol || \"\").toUpperCase();\n const t = String(target || \"\").trim();\n if (!t) return { ok: false, reason: \"target_missing\", shouldReconnect: false };\n if (p === \"RTMP\") {\n const ok = t.startsWith(\"rtmp://\") || t.startsWith(\"rtmps://\");\n return { ok, reason: ok ? \"rtmp_target_format_ok\" : \"invalid_rtmp_target\", shouldReconnect: !ok, reconnectInSec: 15 };\n }\n if (p === \"WHIP\" || p === \"SFU\") {\n try {\n const res = await fetch(t, { method: \"OPTIONS\" });\n const ok = res.status < 500;\n return { ok, reason: `http_${res.status}`, shouldReconnect: !ok, reconnectInSec: 8 };\n } catch {\n return { ok: false, reason: \"network_unreachable\", shouldReconnect: true, reconnectInSec: 8 };\n }\n }\n return { ok: true, reason: \"unchecked_protocol\", shouldReconnect: false };\n}\n\nasync function fetchTextFromUrl(url) {\n const res = await fetch(url, { method: \"GET\" });\n if (!res.ok) throw new Error(`fetch_${res.status}`);\n const ct = String(res.headers.get(\"content-type\") || \"\");\n if (ct.includes(\"application/pdf\")) throw new Error(\"pdf_binary_not_supported_directly\");\n const body = await res.text();\n return stripHtml(body).slice(0, 14000);\n}\n\nfunction buildTaskActionGraph(task = \"\", context = {}) {\n const t = String(task || \"\").toLowerCase();\n if (t.includes(\"dinner\") || t.includes(\"akşam yemeği\") || t.includes(\"aksam yemegi\")) {\n return {\n taskTitle: \"Dinner Prep Planner\",\n nodes: [\n { id: \"n1\", title: \"Kitchen preheat\", action: \"smart-oven.preheat\", params: { temperatureC: 190, mode: \"fan\" } },\n { id: \"n2\", title: \"Lighting comfort\", action: \"smart-light.set_scene\", params: { scene: \"warm_dinner\" } },\n { id: \"n3\", title: \"Fridge inventory check\", action: \"smart-fridge.inventory_scan\", params: {} },\n { id: \"n4\", title: \"Reminder notification\", action: \"smart-watch.notify\", params: { text: \"Dinner prep started.\" } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" },\n { from: \"n3\", to: \"n4\" }\n ]\n };\n }\n return {\n taskTitle: task || \"General Planner\",\n nodes: [\n { id: \"n1\", title: \"Safety check\", action: \"device.safety_check\", params: { context } },\n { id: \"n2\", title: \"Primary action\", action: \"device.execute_task\", params: { task } },\n { id: \"n3\", title: \"Report\", action: \"studio.timeline_note\", params: { task } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" }\n ]\n };\n}\n\nfunction buildEthicsPrograms() {\n return {\n policyVersion: \"v1\",\n tracks: [\n {\n id: \"elderly-care-assist\",\n audience: \"elderly\",\n title: \"Elderly Assistance Track\",\n principles: [\"human-in-the-loop\", \"consent-first\", \"low-friction alerts\"],\n modules: [\"medication-reminder\", \"fall-risk-check\", \"family-notify\", \"daily-companion-brief\"]\n },\n {\n id: \"kids-safe-learning\",\n audience: \"children\",\n title: \"Kids Safe Learning Track\",\n principles: [\"guardian-consent\", \"age-appropriate-language\", \"content-filtering\"],\n modules: [\"study-coach\", \"screen-time-balance\", \"safe-social-share\", \"emergency-contact\"]\n }\n ]\n };\n}\n\nasync function deliverViaTelegram(channel, text) {\n const chatId = String(channel.endpoint || \"\").trim();\n if (!TELEGRAM_BOT_TOKEN) throw new Error(\"telegram_token_missing\");\n if (!chatId) throw new Error(\"telegram_chat_id_missing\");\n const url = `https://api.telegram.org/bot${encodeURIComponent(TELEGRAM_BOT_TOKEN)}/sendMessage`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ chat_id: chatId, text })\n });\n if (!res.ok) throw new Error(`telegram_http_${res.status}`);\n return { provider: \"telegram\", status: res.status };\n}\n\nasync function deliverViaWhatsapp(channel, text) {\n const to = String(channel.endpoint || \"\").trim();\n if (!WHATSAPP_TOKEN) throw new Error(\"whatsapp_token_missing\");\n if (!WHATSAPP_PHONE_NUMBER_ID) throw new Error(\"whatsapp_phone_number_id_missing\");\n if (!to) throw new Error(\"whatsapp_to_missing\");\n const url = `https://graph.facebook.com/v22.0/${encodeURIComponent(WHATSAPP_PHONE_NUMBER_ID)}/messages`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${WHATSAPP_TOKEN}` },\n body: JSON.stringify({ messaging_product: \"whatsapp\", to, type: \"text\", text: { body: text } })\n });\n if (!res.ok) throw new Error(`whatsapp_http_${res.status}`);\n return { provider: \"whatsapp\", status: res.status };\n}\n\nasync function deliverViaWebhook(channel, text) {\n const endpoint = String(channel.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"webhook_endpoint_missing\");\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text, platform: channel.platform || \"webhook\", ts: Date.now() })\n });\n if (!res.ok) throw new Error(`webhook_http_${res.status}`);\n return { provider: \"webhook\", status: res.status };\n}\n\nasync function deliverSocialMessage(channel, text) {\n const platform = String(channel.platform || \"webhook\").toLowerCase();\n if (platform === \"telegram\") return deliverViaTelegram(channel, text);\n if (platform === \"whatsapp\") return deliverViaWhatsapp(channel, text);\n return deliverViaWebhook(channel, text);\n}\n\nasync function deliverSocialWithRetry(channel, text) {\n let attempt = 0;\n let lastError = null;\n while (attempt <= SOCIAL_RETRY_MAX) {\n try {\n const result = await deliverSocialMessage(channel, text);\n return { ok: true, attempt: attempt + 1, ...result };\n } catch (error) {\n lastError = error;\n if (attempt >= SOCIAL_RETRY_MAX) break;\n const jitter = Math.floor(Math.random() * 90);\n await sleep(SOCIAL_RETRY_BASE_MS * Math.pow(2, attempt) + jitter);\n }\n attempt += 1;\n }\n return { ok: false, attempt: attempt + 1, error: String(lastError?.message || \"delivery_failed\") };\n}\n\nasync function dispatchCommandToAdapter(device, command) {\n const adapter = String(command?.adapter || device?.adapter || \"websocket\").toLowerCase();\n const endpoint = String(device?.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"device_endpoint_missing\");\n if (adapter === \"mqtt\") {\n // MQTT delivery is proxied via a bridge endpoint (HTTP webhook style).\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n type: \"mqtt-command\",\n topic: `castle/devices/${device.id}/commands`,\n action: command.action,\n params: command.params || {}\n })\n });\n if (!res.ok) throw new Error(`mqtt_bridge_http_${res.status}`);\n return { ok: true, adapter, via: \"http-bridge\", status: res.status };\n }\n if (!endpoint.startsWith(\"ws://\") && !endpoint.startsWith(\"wss://\")) throw new Error(\"invalid_websocket_endpoint\");\n const sendResult = await new Promise((resolve, reject) => {\n const ws = new WebSocket(endpoint);\n const timer = setTimeout(() => {\n try {\n ws.close();\n } catch {\n /* noop */\n }\n reject(new Error(\"websocket_timeout\"));\n }, 3000);\n ws.on(\"open\", () => {\n ws.send(\n JSON.stringify({\n type: \"castle-command\",\n commandId: command.id,\n deviceId: command.deviceId,\n action: command.action,\n params: command.params || {}\n }),\n (err) => {\n clearTimeout(timer);\n ws.close();\n if (err) reject(new Error(\"websocket_send_failed\"));\n else resolve({ ok: true, adapter: \"websocket\", via: \"ws\" });\n }\n );\n });\n ws.on(\"error\", () => {\n clearTimeout(timer);\n reject(new Error(\"websocket_connect_failed\"));\n });\n });\n return sendResult;\n}\n\nfunction readHttpJson(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => {\n if (!buf) return resolve({});\n const payload = safeJsonParse(buf);\n if (!payload || typeof payload !== \"object\") return reject(new Error(\"invalid_json_payload\"));\n resolve(payload);\n });\n req.on(\"error\", reject);\n });\n}\n\n/** Ham gövde (HMAC doğrulaması için). */\nfunction readHttpBodyText(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => resolve(buf));\n req.on(\"error\", reject);\n });\n}\n\nfunction maskOpaqueId(value) {\n const t = String(value || \"\");\n if (!t) return \"(empty)\";\n if (t.length <= 8) return \"****\";\n return `${t.slice(0, 4)}…${t.slice(-2)}`;\n}\n\nfunction readGatewayHttpToken(req) {\n const x = String(req.headers?.[\"x-castle-gateway-token\"] || \"\").trim();\n if (x) return x;\n const auth = String(req.headers?.authorization || \"\");\n const m = /^Bearer\\s+(.+)$/i.exec(auth);\n return m ? String(m[1] || \"\").trim() : \"\";\n}\n\nasync function resolveHttpUser(req) {\n const authResult = await verifyClientToken(req);\n if (authResult?.ok && authResult.user?.uid) {\n return { ok: true, uid: String(authResult.user.uid), mode: authResult.kind || \"auth\" };\n }\n if (ALLOW_DEV_ANON && ALLOW_DEV_HTTP_UID) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) return { ok: true, uid: `dev-${devUid.slice(0, 64)}`, mode: \"dev-header\" };\n }\n return { ok: false, reason: authResult?.reason || \"auth_required\" };\n}\n\n/**\n * Epistemic ledger ingest — Firebase uid OR valid transport token + X-Castle-Dev-Uid.\n * Prod disables CASTLE_ALLOW_DEV_HTTP_UID; LLM may still work while telemetry 401'd without this.\n */\nasync function resolveEpistemicIngestActor(req) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok && tok === REQUIRED_GATEWAY_TOKEN) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) {\n return { ok: true, uid: `gw-telemetry-${devUid.slice(0, 64)}`, mode: \"gateway-token-dev-uid\" };\n }\n return { ok: false, reason: \"dev_uid_required\" };\n }\n if (tok) {\n return { ok: false, reason: \"gateway_token_mismatch\" };\n }\n }\n const auth = await resolveHttpUser(req);\n if (auth.ok) return auth;\n return { ok: false, reason: auth.reason || \"auth_required\" };\n}\n\n/** Mesh actor: Firebase uid, dev header, or `guest:<uuid>` via header / `guestId` query (SSE cannot set headers). */\nasync function resolveMeshActor(req) {\n const auth = await resolveHttpUser(req);\n if (auth.ok) return { ok: true, clientUid: auth.uid, mode: auth.mode };\n if (ALLOW_MESH_GUEST) {\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const gid = String(req.headers?.[\"x-castle-guest-id\"] || q.searchParams.get(\"guestId\") || \"\").trim();\n if (gid) {\n const normalized = gid.startsWith(\"guest:\") ? gid.slice(0, 180) : `guest:${gid.slice(0, 160)}`;\n return { ok: true, clientUid: normalized, mode: \"guest-header\" };\n }\n }\n return { ok: false, reason: \"mesh_auth_required\" };\n}\n\n/** Live genesis runtime JSON (GET /rhizoh/genesis/runtime + SSE infra sampler). */\nasync function buildGenesisRuntimeSurfacePayloadLive() {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n let spiralWsActive = 0;\n if (spiralWssForGenesis) {\n for (const c of spiralWssForGenesis.clients) {\n if (c.readyState === 1) spiralWsActive += 1;\n }\n }\n const base = buildGenesisRuntimeSurfacePayload({\n infraMetrics,\n scoredHealth: scored,\n rhizohEnterpriseMetrics,\n mesh: meshContinuityAggregate(),\n workerHealth: worker,\n port: PORT,\n spiralWebSocketClientsActive: spiralWsActive\n });\n return {\n ...base,\n replayFingerprint: computeGenesisReplayFingerprintV0(base),\n gatewayCapabilities: buildGenesisGatewayCapabilitiesV0(),\n genesisStream: {\n schema: \"castle.genesis.stream_cursor.v0\",\n lastAcceptedSeq: getGenesisContinuitySeq(),\n clientIngress: {\n schema: GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n contract: \"castle.world_observation.ingress_envelope.v1\",\n postPath: rhizohRuntime.routes.genesisIngress,\n closure: getGenesisWorldObservationIngressStatusV0()\n },\n seqContinuity: getGenesisContinuitySeqAuditV0(),\n eventArchive: genesisContinuityEventArchiveEnabled()\n ? {\n enabled: true,\n projection: GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION,\n queryPath: rhizohRuntime.routes.genesisContinuityEvents\n }\n : { enabled: false },\n replayRouter: {\n schema: GENESIS_REPLAY_ROUTER_SCHEMA,\n queryPath: rhizohRuntime.routes.genesisReplay,\n queryHint: \"from=&to= or range=from-to; optional type=&checkpoints=0\",\n temporalDiffPath: rhizohRuntime.routes.genesisReplayDiff,\n temporalDiffSchema: GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA,\n equivalencePath: rhizohRuntime.routes.genesisReplayEquivalence,\n analyticsPath: rhizohRuntime.routes.genesisReplayAnalytics,\n evolutionPath: rhizohRuntime.routes.genesisReplayEvolution\n }\n }\n };\n}\n\nconst httpServer = createServer(async (req, res) => {\n const corsAllow = applyHttpCorsHeaders(req, res);\n if (req.method === \"OPTIONS\") {\n if (!corsAllow && req.headers?.origin) {\n console.warn(\"[GATEWAY_CORS] preflight rejected for origin:\", req.headers.origin);\n }\n res.writeHead(corsAllow ? 204 : 403);\n res.end();\n return;\n }\n\n const pathname = getHttpPathname(req);\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/genesis/__ping\") {\n sendJson(res, 200, {\n ok: true,\n pid: process.pid,\n entry: __filename,\n time: Date.now(),\n genesis: true\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/epistemic/__probe\") {\n sendJson(res, 200, {\n ok: true,\n epistemicIngestActorV2: true,\n routes: {\n seal: rhizohRuntime.routes.epistemicSeal,\n logsBatch: rhizohRuntime.routes.epistemicLogsBatch\n },\n hasGatewayToken: Boolean(REQUIRED_GATEWAY_TOKEN),\n gatewayTokenLen: REQUIRED_GATEWAY_TOKEN ? REQUIRED_GATEWAY_TOKEN.length : 0\n });\n return;\n }\n\n if (\n req.method === \"GET\" &&\n (pathname === \"/rhizoh/live/world-feed\" ||\n pathname === \"/rhizoh/live/sports-bundle\" ||\n pathname === \"/rhizoh/live/news-headlines\")\n ) {\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_live_world_feed:${ip}`, RL_RHIZOH_LIVE_WORLD_FEED_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n try {\n const out =\n pathname === \"/rhizoh/live/sports-bundle\"\n ? await handleLiveSportsBundleGetV0()\n : pathname === \"/rhizoh/live/news-headlines\"\n ? await handleLiveNewsHeadlinesGetV0(req)\n : await handleLiveWorldFeedGetV0(req);\n sendJson(res, out.status, out.body);\n } catch (e) {\n sendJson(res, 502, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/webhooks/stripe\") {\n await handleStripeMembershipWebhook(req, res);\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/billing/stripe/checkout\") {\n await handleStripeCheckoutCreate(req, res, { readHttpJson, resolveHttpUser, sendJson });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/live\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, live: false, dns: true, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n sendJson(res, 200, { ok: true, live: true, dns: true, service: \"castle-gateway\", ts: Date.now() });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/cors\") {\n const reqOrigin = normalizeHttpOrigin(req.headers?.origin);\n const allowOrigin = accessControlAllowOriginValue(req);\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n requestOrigin: reqOrigin || null,\n allowed: Boolean(allowOrigin),\n allowOrigin: allowOrigin || null,\n allowSetSize: httpCors.allowSet.size\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/ready\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, ready: false, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n try {\n const persistence = getFirebasePersistence();\n const llmProbe = rhizohLlmEnvConfigured();\n sendJson(res, 200, {\n ok: true,\n ready: true,\n dns: true,\n persistence: persistence.mode,\n service: \"castle-gateway\",\n gateway: \"ok\",\n llm: llmProbe ? \"ok\" : \"degraded\",\n spine: llmProbe ? \"ok\" : \"partial\"\n });\n } catch (e) {\n sendJson(res, 503, { ok: false, ready: false, reason: String(e?.message || e), service: \"castle-gateway\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/reality\") {\n const payload = buildRealityHealthPayload();\n sendJson(res, payload.ok ? 200 : 503, payload);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/substrate\") {\n const authority = buildGatewaySubstrateAuthoritySnapshotV0();\n const operational = buildSubstrateOperationalSnapshotV0();\n const drift = buildRealityHealthPayload();\n const ok = Boolean(authority.productionAuthoritySatisfied) && drift.ok;\n sendJson(res, ok ? 200 : 503, {\n ok,\n service: \"castle-gateway\",\n ts: Date.now(),\n authority,\n operational,\n drift: { ok: drift.ok, errors: drift.drift?.errors, warnings: drift.drift?.warnings }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/substrate/health\") {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const body = await readHttpJson(req);\n const uid = (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestClientSubstrateHealthV0(body, { uid, clientId: uid });\n if (!result.ok) {\n sendJson(res, 400, result);\n return;\n }\n recordSubstrateOtelEventV0(\"client_health_ingest\", { uid: String(uid).slice(0, 32) });\n sendJson(res, 200, result);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/deps\") {\n const t0 = Date.now();\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, {\n ok: false,\n dns: true,\n llm: false,\n firestore: false,\n phase: \"maintenance\",\n latencyMs: Date.now() - t0,\n service: \"castle-gateway\"\n });\n return;\n }\n const persistence = getFirebasePersistence();\n let firestoreOk = persistence.mode !== \"firebase\";\n let firestoreDetail = persistence.mode === \"file\" ? \"file_mode\" : \"not_applicable\";\n if (persistence.mode === \"firebase\" && persistence.db) {\n const pr = await probeFirestoreReachable(persistence.db, 2000);\n firestoreOk = pr.ok;\n firestoreDetail = pr.reason || (pr.ok ? \"reachable\" : \"fail\");\n } else if (persistence.mode === \"firebase\" && !persistence.db) {\n firestoreOk = false;\n firestoreDetail = \"firebase_not_initialized\";\n }\n const llmOk = rhizohLlmEnvConfigured();\n const overallOk = llmOk && firestoreOk;\n const latencyMs = Date.now() - t0;\n sendJson(res, 200, {\n ok: overallOk,\n dns: true,\n llm: llmOk,\n firestore: persistence.mode === \"firebase\" ? firestoreOk : false,\n persistence: persistence.mode,\n firestoreDetail,\n latencyMs,\n service: \"castle-gateway\",\n wsPort: PORT\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health\") {\n const persistence = getFirebasePersistence();\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n wsPort: PORT,\n persistence: persistence.mode,\n presenceMesh: \"sse\"\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/health\") {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n sendJson(res, 200, {\n ok: true,\n role: \"gateway\",\n queueLagMs: infraMetrics.queueLag,\n queueDepth: infraMetrics.queueDepth,\n errors: infraMetrics.errors,\n status: scored.status,\n score: scored.score,\n reasons: scored.reasons,\n worker: worker\n ? {\n status: worker.status,\n score: worker.score,\n reasons: worker.reasons\n }\n : null,\n timestamp: Date.now()\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisRuntime) {\n const payload = await buildGenesisRuntimeSurfacePayloadLive();\n sendJson(res, 200, payload);\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.genesisIngress) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const actor = await resolveMeshActor(req);\n const clientId = actor.ok ? actor.clientUid : (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestGenesisWorldObservationV0(body, { clientId });\n if (!result.ok) {\n const status =\n result.error === \"rate_limited\" || result.error === \"agent_spoke_throttled\"\n ? 429\n : result.error === \"payload_too_large\"\n ? 413\n : 400;\n sendJson(res, status, result);\n return;\n }\n sendJson(res, result.idempotent ? 200 : 202, result);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisStream) {\n const corsOrigin = accessControlAllowOriginValue(req);\n if (!corsOrigin && String(process.env.NODE_ENV || \"\").toLowerCase() === \"production\") {\n const og = String(req.headers?.origin || \"\").trim();\n if (og) {\n console.warn(\"[GATEWAY] genesis SSE: missing Access-Control-Allow-Origin for browser Origin (CORS whitelist mismatch):\", og);\n }\n }\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n res.write(\"retry: 3000\\n\\n\");\n res.write(\": genesis continuity stream\\n\\n\");\n res.write(\"event: genesis\\n\");\n res.write(`data: {\"booted\":true,\"pid\":${process.pid}}\\n\\n`);\n registerGenesisSseClient(res, req);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLatest) {\n const cp = getLatestGenesisSignedCheckpoint();\n if (!cp) {\n sendJson(res, 404, { ok: false, error: \"no_checkpoint_yet\", hint: \"wait_for_seq_interval_or_configure_signing_secret\" });\n return;\n }\n sendJson(res, 200, { ok: true, checkpoint: cp });\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(rhizohRuntime.routes.genesisCheckpointBySeqPrefix)) {\n const tail = pathname.slice(rhizohRuntime.routes.genesisCheckpointBySeqPrefix.length).split(\"/\")[0];\n const seq = parseInt(String(tail || \"\").trim(), 10);\n const out = await genesisCheckpointQueryBySeqV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"checkpoint_not_found\") {\n sendJson(res, 404, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointRange) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryRangeV0(from, to);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"range_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLineage) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const seq = parseInt(String(u.searchParams.get(\"seq\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryLineageV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"lineage_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisContinuityEvents) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const limit = parseInt(String(u.searchParams.get(\"limit\") || \"\").trim(), 10);\n const out = await queryGenesisContinuityEventArchiveV0(from, to, type, limit);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\" || out.error === \"event_archive_disabled\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"archive_file_too_large\") {\n sendJson(res, 413, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplay) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await resolveGenesisReplayRouterV1({\n from: fromSeq,\n to: toSeq,\n type,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayDiff) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const out = await computeGenesisReplayTemporalDiffV1({ from: fromSeq, to: toSeq, type });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEquivalence) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from1 = parseInt(String(u.searchParams.get(\"from1\") || \"\").trim(), 10);\n const to1 = parseInt(String(u.searchParams.get(\"to1\") || \"\").trim(), 10);\n const from2 = parseInt(String(u.searchParams.get(\"from2\") || \"\").trim(), 10);\n const to2 = parseInt(String(u.searchParams.get(\"to2\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await compareGenesisReplayEquivalenceV1({\n from1,\n to1,\n from2,\n to2,\n type,\n includeCheckpoints\n });\n if (\n !out.ok &&\n (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")\n ) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayAnalytics) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayAnalyticsV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEvolution) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const collapseWindows = parseInt(String(u.searchParams.get(\"collapseWindows\") || \"0\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayEvolutionV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n collapseWindows,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/metrics\") {\n res.writeHead(200, { \"Content-Type\": \"text/plain; version=0.0.4; charset=utf-8\" });\n res.end(renderInfraMetricsProm());\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.infraPrometheusMetrics) {\n if (process.env.CASTLE_PROMETHEUS_METRICS !== \"1\") {\n sendJson(res, 404, { ok: false, error: \"prometheus_metrics_disabled\" });\n return;\n }\n res.writeHead(200, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(renderRhizohPrometheusMetrics());\n return;\n }\n if (req.method === \"GET\" && pathname.startsWith(\"/infra/traces/\")) {\n const traceId = decodeURIComponent(pathname.slice(\"/infra/traces/\".length));\n const item = await getTraceById(traceId);\n if (!item) return sendJson(res, 404, { ok: false, error: \"trace_not_found\" });\n return sendJson(res, 200, { ok: true, trace: item });\n }\n if (req.method === \"GET\" && pathname === \"/infra/traces\") {\n const u = new URL(String(req.url || \"/infra/traces\"), `http://${req.headers.host || \"localhost\"}`);\n const sessionId = String(u.searchParams.get(\"sessionId\") || \"\").trim();\n const items = sessionId ? await listTracesBySession(sessionId) : [];\n return sendJson(res, 200, { ok: true, sessionId, traces: items });\n }\n\n /** Presence room mesh (C): JOIN / LEAVE / SNAPSHOT / DELTA / REPLAY (HTTP); SUBSCRIBE = SSE stream. */\n if (req.method === \"POST\" && pathname === \"/presence/mesh/join\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid, clientUid: actor.clientUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/leave\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshLeave(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/snapshot\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n sendJson(res, 200, meshSnapshot(roomUid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/delta\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, MAX_MESSAGE_BYTES);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const seq = meshAppendDelta(roomUid, actor.clientUid, {\n node: body?.node,\n projectionPatch: body?.projectionPatch,\n writerSubject: body?.writerSubject\n });\n sendJson(res, 200, { ok: true, roomUid, seq });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/replay\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const fromNodeId = body?.fromNodeId != null ? String(body.fromNodeId) : \"\";\n const fromSeq = body?.fromSeq;\n if (fromNodeId) {\n sendJson(res, 200, meshReplayFromNodeId(roomUid, fromNodeId));\n return;\n }\n sendJson(res, 200, meshReplayFromSeq(roomUid, fromSeq));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/presence/mesh/subscribe\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const roomUid = String(q.searchParams.get(\"roomUid\") || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n const write = (chunk) => {\n try {\n res.write(chunk);\n } catch {\n /* closed */\n }\n };\n const unsub = meshSubscribeSse(roomUid, write);\n const onClose = () => {\n unsub();\n meshLeave(roomUid, actor.clientUid);\n try {\n res.end();\n } catch {\n /* ignore */\n }\n };\n req.on(\"close\", onClose);\n req.on(\"aborted\", onClose);\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, uid: auth.uid, mode: auth.mode, items: await listConnections(auth.uid) });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const id = await createConnection(auth.uid, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"create_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n await updateConnection(auth.uid, id, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"update_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"DELETE\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n await deleteConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"delete_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections/test\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n if (!checkHttpRateLimit(`llm_conn_test:${auth.uid}`, RL_LLM_CONN_TEST_PER_MIN, 60_000)) {\n return sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n }\n const hasBodyKey = !!String(payload?.apiKey || \"\").trim();\n const result = await queryRhizohLlm(\n {\n message: String(payload?.message || \"Ping from Castle Gateway\"),\n context: payload?.context || { source: \"connection_test\" },\n provider: payload?.provider,\n model: payload?.model,\n apiKey: payload?.apiKey || \"\"\n },\n { allowExternalApiKey: true, llmKeySource: hasBodyKey ? \"auto\" : \"env\" }\n );\n logLlmAccess({\n route: \"/llm/connections/test\",\n uid: auth.uid,\n llmKeyBillingOwner: result.llmKeyBillingOwner,\n llmKeyOrigin: result.llmKeyOrigin,\n provider: result.provider,\n model: result.model,\n connectionId: null\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"connection_test_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAgentIdentities(auth.uid);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerAgentIdentity({\n ownerUid: auth.uid,\n agentId: payload?.agentId,\n role: payload?.role,\n personaSeed: payload?.personaSeed || {},\n capabilityLevel: payload?.capabilityLevel\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/agents/identities/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const owns = await getAgentIdentity(id);\n if (!owns || owns.ownerUid !== auth.uid) return sendJson(res, 403, { ok: false, error: \"forbidden\" });\n const payload = await readHttpJson(req);\n const row = await updateAgentIdentity(id, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/academy/events\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/academy/events\"}`);\n const status = String(url.searchParams.get(\"status\") || \"\");\n const items = await listAcademyEvents({ ownerUid: auth.uid, status, limit: 120 });\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/academy/events\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const ev = await queueAcademyEvent({\n type: payload?.type,\n roomId: payload?.roomId,\n ownerUid: auth.uid,\n participants: payload?.participants || [],\n topic: payload?.topic || \"academy\",\n payload: payload?.payload || {}\n });\n sendJson(res, 200, { ok: true, event: ev });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && String(req.url || \"\").startsWith(\"/academy/events/\") && String(req.url || \"\").endsWith(\"/resolve\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const parts = String(req.url || \"\").split(\"/\").filter(Boolean);\n const eventId = parts[2] || \"\";\n try {\n const payload = await readHttpJson(req);\n const out = await resolveAcademyEvent(eventId, payload || {});\n sendJson(res, 200, { ok: true, event: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_resolve_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/memory\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/memory\"}`);\n const scope = url.searchParams.get(\"scope\") === \"agents\" ? \"agents\" : \"users\";\n const id = String(url.searchParams.get(\"id\") || (scope === \"agents\" ? \"\" : auth.uid));\n const kind = String(url.searchParams.get(\"kind\") || \"\");\n const limit = Number(url.searchParams.get(\"limit\") || 40);\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n sendJson(res, 200, { ok: true, scope, id, kind, items: await listMemories({ scope, id, kind, limit }) });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const profile = await getPersonaGoalMemory(auth.uid);\n sendJson(res, 200, { ok: true, profile });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const profile = await setPersonaGoalMemory({\n uid: auth.uid,\n personaPatch: payload?.personaPatch || {},\n goals: Array.isArray(payload?.goals) ? payload.goals : null,\n preferencesPatch: payload?.preferencesPatch || {}\n });\n sendJson(res, 200, { ok: true, profile });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"profile_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/compact\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const out = await autoCompactMemories({ scope, id });\n sendJson(res, 200, { ok: true, scope, id, ...out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_compact_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/context\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const context = await getMemoryContext({\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n query: String(payload?.query || \"\"),\n limit: Number(payload?.limit || 80)\n });\n sendJson(res, 200, { ok: true, context });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_context_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/place-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const placeName = String(payload?.placeName || \"\").trim();\n if (!placeName) return sendJson(res, 400, { ok: false, error: \"placeName_required\" });\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const persona = String(payload?.waypointPersona || payload?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(placeName)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Place: ${placeName}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Give a concise conversational briefing about this place for a walking-route companion.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-place-brief\", placeName, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `PLACE_BRIEF:${placeName} => ${String(summary.reply || \"\").slice(0, 900)}`,\n tags: [\"place\", \"brief\", \"event-layer\"],\n importance: 0.62,\n meta: { placeName }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"place-brief\",\n text: `${placeName} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: {\n placeName,\n directive: summary.directive,\n lat: hit?.lat ? Number(hit.lat) : null,\n lon: hit?.lon ? Number(hit.lon) : null\n }\n });\n sendJson(res, 200, { ok: true, place: hit || null, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"place_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/route-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const waypoints = Array.isArray(payload?.waypoints) ? payload.waypoints.slice(0, 24) : [];\n const out = [];\n for (const wp of waypoints) {\n const name = String(wp?.name || \"\").trim();\n if (!name) continue;\n const persona = String(wp?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(name)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Waypoint: ${name}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Generate a concise spoken briefing for route waypoint.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-route-brief\", waypoint: name, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n out.push({\n name,\n persona,\n place: hit || null,\n reply: summary.reply,\n directive: summary.directive\n });\n }\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `ROUTE_BRIEF: waypoints=${out.length} ${out.map((x) => `${x.name}:${x.persona}`).join(\" | \")}`.slice(0, 1300),\n tags: [\"route\", \"waypoint\", \"brief\", \"event-layer\"],\n importance: 0.74,\n meta: { count: out.length }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"route-brief\",\n text: out.map((x) => `${x.name}:${String(x.reply || \"\").slice(0, 140)}`).join(\" | \").slice(0, 1400),\n roomId: \"studio-main\",\n meta: {\n waypointCount: out.length,\n waypoints: out.map((x) => ({\n name: x.name,\n persona: x.persona,\n lat: x.place?.lat ? Number(x.place.lat) : null,\n lon: x.place?.lon ? Number(x.place.lon) : null\n }))\n }\n });\n sendJson(res, 200, { ok: true, items: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"route_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/pdf-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const title = String(payload?.title || \"document\");\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const providedText = String(payload?.text || \"\");\n const url = String(payload?.url || \"\");\n let content = providedText;\n if (!content && url) content = await fetchTextFromUrl(url);\n if (!content) return sendJson(res, 400, { ok: false, error: \"text_or_supported_url_required\" });\n const summary = await queryRhizohLlm(\n {\n message: `Summarize this document and provide key points + practical actions.\\nTitle:${title}\\nContent:\\n${content.slice(0, 12000)}`,\n context: { source: \"event-layer-pdf-brief\", title },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `DOC_BRIEF:${title} => ${String(summary.reply || \"\").slice(0, 1000)}`,\n tags: [\"document\", \"brief\", \"library\", \"event-layer\"],\n importance: 0.7,\n meta: { title, url: url || null }\n });\n await addTranscript(auth.uid, {\n source: \"library-brief\",\n eventType: \"pdf-brief\",\n text: `${title} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { title }\n });\n sendJson(res, 200, { ok: true, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/pdf/upload\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req, 8 * 1024 * 1024);\n const job = await enqueuePdfJob({\n uid: auth.uid,\n fileName: payload?.fileName || \"document.pdf\",\n bytesBase64: payload?.contentBase64\n });\n sendJson(res, 200, { ok: true, job });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_upload_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/session\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createPublishSession(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session\",\n text: `Publish session armed: ${row.protocol} ${row.target}`.slice(0, 500),\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: row.id, bridge: row.bridge }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/studio/publish/sessions\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listPublishSessions(auth.uid, 80);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/health-check\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const protocol = String(payload?.protocol || \"WHIP\");\n const target = String(payload?.target || \"\");\n const health = await checkPublishTargetHealth(protocol, target);\n if (payload?.sessionId) {\n await updatePublishSession(auth.uid, String(payload.sessionId), {\n status: health.ok ? \"healthy\" : \"degraded\",\n reconnectPolicy: {\n enabled: true,\n backoffSec: [2, 5, 10, 20],\n nextRetryInSec: health.reconnectInSec || 0,\n maxRetries: 8,\n reason: health.reason\n }\n });\n }\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-health\",\n text: `${protocol} ${target} => ${health.ok ? \"healthy\" : \"degraded\"} (${health.reason})`,\n roomId: \"studio-main\",\n meta: { protocol, target, reconnect: !!health.shouldReconnect }\n });\n sendJson(res, 200, { ok: true, health });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_health_check_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/studio/publish/sessions/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updatePublishSession(auth.uid, id, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session-update\",\n text: `Session ${id} -> ${row.status || \"updated\"}`,\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: id, status: row.status, ingestUrl: row.ingestUrl || \"\" }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/transcripts\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/transcripts\"}`);\n const limit = Math.max(1, Math.min(200, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await listTranscripts(auth.uid, limit);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/transcripts\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await addTranscript(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"transcript_write_failed\" });\n }\n return;\n }\n\n /** E1: broadcast intent → GreenRoom capability → transcript + memory artifact */\n if (req.method === \"POST\" && req.url === \"/studio/capabilities/greenroom\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const traceId = String(payload?.traceId || `TRC-${Date.now().toString(36).toUpperCase()}`);\n const title = String(payload?.title || \"Castle Live Broadcast\").slice(0, 200);\n const intentRaw = String(payload?.intentRaw || \"\").slice(0, 800);\n const audienceEstimate = Math.max(0, Math.min(50_000_000, Number(payload?.audienceEstimate) || 1200));\n const roomId = String(payload?.roomId || \"greenroom-main\");\n const transcriptRow = await addTranscript(auth.uid, {\n source: \"rhizoh\",\n eventType: \"BROADCAST_ROUTED\",\n text: `GreenRoom · ${title}${intentRaw ? `\\nIntent: ${intentRaw}` : \"\"}`,\n roomId,\n meta: {\n ack: \"BROADCAST_ROUTED\",\n capability: \"greenroom\",\n intentType: \"broadcast\",\n traceId,\n audienceEstimate,\n artifact: {\n kind: \"broadcast_card\",\n title,\n traceId,\n roomId,\n createdAt: Date.now()\n }\n }\n });\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n text: `GREENROOM_BROADCAST:${title} trace=${traceId}`,\n tags: [\"broadcast\", \"greenroom\", \"artifact\", \"castle-library\"],\n importance: 0.72,\n kind: \"episodic\",\n meta: { traceId, capability: \"greenroom\", transcriptId: transcriptRow.id }\n });\n sendJson(res, 200, {\n ok: true,\n ack: \"BROADCAST_ROUTED\",\n traceId,\n transcript: transcriptRow,\n replayPath: `/replay/${encodeURIComponent(traceId)}`,\n sharePath: `/replay/${encodeURIComponent(traceId)}`\n });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"greenroom_capability_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/metadata/search\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/metadata/search\"}`);\n const q = String(url.searchParams.get(\"q\") || \"\");\n const limit = Math.max(1, Math.min(300, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await searchMetadata(auth.uid, q, limit);\n sendJson(res, 200, { ok: true, q, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerDevice(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, device: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"device_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const lat = Number(payload?.lat);\n const lon = Number(payload?.lon);\n const speed = Math.max(0, Number(payload?.speed || 0));\n const cfgCenterLat = Number(process.env.CASTLE_GEOFENCE_CENTER_LAT || 41.0082);\n const cfgCenterLon = Number(process.env.CASTLE_GEOFENCE_CENTER_LON || 28.9784);\n const cfgRadiusM = Math.max(100, Number(process.env.CASTLE_GEOFENCE_RADIUS_M || 55000));\n const cfgSpeedCap = Math.max(1, Number(process.env.CASTLE_SPEED_CAP_MPS || 35));\n const dist = Number.isFinite(lat) && Number.isFinite(lon) ? haversineMeters(cfgCenterLat, cfgCenterLon, lat, lon) : 0;\n const geofenceOk = !Number.isFinite(dist) ? true : dist <= cfgRadiusM;\n const speedOk = speed <= cfgSpeedCap;\n const failsafe = !(geofenceOk && speedOk);\n const safety = {\n geofenceOk,\n speedOk,\n failsafe,\n distanceFromCenterM: Number.isFinite(dist) ? dist : null,\n speedCapMps: cfgSpeedCap\n };\n const row = await appendTelemetry(auth.uid, deviceId, {\n lat,\n lon,\n speed,\n battery: payload?.battery,\n mode: payload?.mode || (failsafe ? \"FAILSAFE_HOLD\" : \"ACTIVE\"),\n raw: payload?.raw || {}\n });\n await updateDevice(auth.uid, deviceId, {\n status: failsafe ? \"failsafe\" : \"active\",\n lastTelemetryAt: row.ts,\n safetyState: safety\n });\n if (failsafe) {\n await addTranscript(auth.uid, {\n source: \"robotics-guardrails\",\n eventType: \"failsafe\",\n text: `Device ${deviceId} failsafe triggered geofenceOk=${geofenceOk} speedOk=${speedOk}`,\n roomId: \"studio-main\",\n meta: { deviceId, safety }\n });\n }\n sendJson(res, 200, { ok: true, telemetry: row, safety, command: failsafe ? \"FAILSAFE_HOLD\" : \"CONTINUE\" });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"telemetry_ingest_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listTelemetry(auth.uid, 150);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createAutomation(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"automation-scheduled\",\n text: `Automation scheduled: ${row.title} at ${new Date(row.scheduleAt).toLocaleString()}`,\n roomId: \"studio-main\",\n meta: { automationId: row.id, action: row.action, deviceId: row.deviceId }\n });\n sendJson(res, 200, { ok: true, automation: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"automation_create_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAutomations(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listDevices(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const devs = await listDevices(auth.uid, 500);\n const d = devs.find((x) => x.id === deviceId);\n if (!d) return sendJson(res, 404, { ok: false, error: \"device_not_found\" });\n const action = String(payload?.action || \"\");\n const caps = Array.isArray(d.capabilityProfile) ? d.capabilityProfile : [];\n if (action && caps.length && !caps.includes(action) && !caps.includes(\"*\")) {\n return sendJson(res, 400, { ok: false, error: \"unsupported_action_for_device\", capabilityProfile: caps });\n }\n const cmd = await enqueueCommand(auth.uid, {\n deviceId,\n action: action || \"NOOP\",\n params: payload?.params || {},\n adapter: payload?.adapter || d.adapter || \"websocket\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-command-queue\",\n eventType: \"command-queued\",\n text: `${cmd.deviceId} -> ${cmd.action}`,\n roomId: \"studio-main\",\n meta: { commandId: cmd.id, adapter: cmd.adapter }\n });\n sendJson(res, 200, { ok: true, command: cmd });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listCommands(auth.uid, 200);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/robotics/commands/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updateCommandStatus(auth.uid, id, payload?.status || \"sent\");\n sendJson(res, 200, { ok: true, command: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/rhizoh/bridge\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const result = await runRhizohBrainV2({\n message: String(payload?.message || \"robotics bridge command\"),\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n worldState: payload?.worldState || {},\n deviceState: payload?.deviceState || {},\n intent: \"robotics-mechanics\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-bridge\",\n eventType: \"robotics-command\",\n text: `Bridge -> ${String(result.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { directive: result.directive, agentId: payload?.agentId || \"\" }\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"robotics_bridge_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/planner/graph\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const graph = buildTaskActionGraph(String(payload?.task || \"\"), payload?.context || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"planner-graph\",\n text: `Planner graph built: ${graph.taskTitle} nodes=${graph.nodes.length}`,\n roomId: \"studio-main\",\n meta: { taskTitle: graph.taskTitle, nodeCount: graph.nodes.length }\n });\n sendJson(res, 200, { ok: true, graph });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"planner_graph_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerSocialChannel(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, channel: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_channel_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listSocialChannels(auth.uid, 100);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/broadcast\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const text = String(payload?.text || \"\").slice(0, 1200);\n const channels = await listSocialChannels(auth.uid, 100);\n const enabled = channels.filter((c) => c.status === \"enabled\");\n const deliveries = [];\n for (const c of enabled) {\n const out = await deliverSocialWithRetry(c, text);\n deliveries.push({ id: c.id, platform: c.platform, ...out });\n }\n const okCount = deliveries.filter((d) => d.ok).length;\n await addTranscript(auth.uid, {\n source: \"social-layer\",\n eventType: \"social-broadcast\",\n text: `Broadcast delivered ${okCount}/${enabled.length} channel(s): ${text.slice(0, 240)}`,\n roomId: \"studio-main\",\n meta: { deliveries }\n });\n sendJson(res, 200, { ok: true, sentChannels: okCount, totalChannels: enabled.length, deliveries });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_broadcast_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/ethics/programs\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, ...buildEthicsPrograms() });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands/dispatch\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const all = await listCommands(auth.uid, 200);\n const queued = all.filter((c) => String(c.status || \"queued\") === \"queued\").slice(0, 50);\n const devices = await listDevices(auth.uid, 500);\n const byId = new Map(devices.map((d) => [d.id, d]));\n const results = [];\n for (const cmd of queued) {\n const dev = byId.get(cmd.deviceId);\n if (!dev) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: \"device_not_found\" });\n continue;\n }\n try {\n const dispatched = await dispatchCommandToAdapter(dev, cmd);\n await updateCommandStatus(auth.uid, cmd.id, \"sent\");\n results.push({ id: cmd.id, ok: true, adapter: dispatched.adapter, via: dispatched.via });\n } catch (error) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: String(error?.message || \"dispatch_failed\") });\n }\n }\n await addTranscript(auth.uid, {\n source: \"robotics-command-dispatch\",\n eventType: \"command-dispatch-batch\",\n text: `Dispatch batch size=${results.length} ok=${results.filter((x) => x.ok).length}`,\n roomId: \"studio-main\",\n meta: { results: results.slice(0, 30) }\n });\n sendJson(res, 200, { ok: true, processed: results.length, results });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"dispatch_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/pdf/jobs/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n const job = await getPdfJob(auth.uid, id);\n if (!job) return sendJson(res, 404, { ok: false, error: \"pdf_job_not_found\" });\n sendJson(res, 200, { ok: true, job });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const row = await appendMemory({\n scope,\n id,\n text: payload?.text,\n tags: payload?.tags || [],\n importance: payload?.importance,\n kind: payload?.kind || \"episodic\",\n meta: { uid: auth.uid, ...(payload?.meta || {}) }\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.externalTruth) {\n try {\n if (process.env.RHIZOH_EXTERNAL_TRUTH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_truth:${ip}`, RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n sendJson(res, 200, buildRhizohExternalGroundTruthPayload(req));\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.productOutcome) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const rawBody = await readHttpBodyText(req, 64 * 1024);\n const body = rawBody.trim() ? safeJsonParse(rawBody) : null;\n if (!body || typeof body !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"invalid_json_payload\" });\n return;\n }\n const sigHdr = String(req.headers?.[\"x-rhizoh-outcome-signature\"] || \"\").trim();\n const hmacV = verifyRhizohOutcomeHmac(rawBody, sigHdr);\n if (!hmacV.ok) {\n sendJson(res, 401, { ok: false, error: hmacV.error || \"outcome_hmac_invalid\" });\n return;\n }\n const srcTokHdr = String(req.headers?.[\"x-rhizoh-outcome-source-token\"] || \"\").trim();\n const srcV = verifyRhizohOutcomeSourceToken(body.source, srcTokHdr);\n if (!srcV.ok) {\n sendJson(res, 401, { ok: false, error: srcV.error || \"outcome_source_token_invalid\" });\n return;\n }\n const subjectKey = String(body.subjectKey || \"\").trim().slice(0, 128);\n if (\n subjectKey &&\n !checkHttpRateLimit(\n `rhizoh_product_outcome_subj:${subjectKey}`,\n RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN,\n 60_000\n )\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_subject\" });\n return;\n }\n const result = ingestRhizohProductOutcomeHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n schemaVersion: \"1.1.0\",\n aggregate: result.aggregate,\n integrity: result.integrity\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"outcome_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.productOutcomeAggregate) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome_agg:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const u = new URL(String(req.url || \"/\"), \"http://localhost\");\n const cohortId = u.searchParams.get(\"cohortId\") || \"\";\n const decisionFingerprint = u.searchParams.get(\"decisionFingerprint\") || \"\";\n const limit = u.searchParams.get(\"limit\") || \"50\";\n const rows = listRhizohProductOutcomeAggregates({\n cohortId: cohortId || undefined,\n decisionFingerprint: decisionFingerprint || undefined,\n limit: Number(limit) || 50\n });\n sendJson(res, 200, { ok: true, schemaVersion: \"1.1.0\", rows });\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.externalLossBatch) {\n try {\n if (process.env.RHIZOH_EXTERNAL_LOSS_BATCH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_loss_batch:${ip}`, RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const result = ingestRhizohExternalLossBatchHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, { ok: true, accepted: result.accepted ?? 0 });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"batch_failed\") });\n }\n return;\n }\n\n function moderationAdminOk(req) {\n const expected = String(process.env.CASTLE_MODERATION_ADMIN_KEY || \"\").trim();\n if (!expected) return false;\n const got = String(req.headers[\"x-castle-moderation-key\"] || \"\").trim();\n return got.length > 0 && got === expected;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.opsAbuseReport) {\n try {\n const body = await readHttpJson(req, 32 * 1024);\n const auth = await resolveHttpUser(req);\n const entry = submitAbuseReportV0({\n reporterUid: auth.ok ? auth.uid : null,\n targetUid: body?.targetUid ?? null,\n category: body?.category,\n detail: body?.detail,\n traceId: body?.traceId ?? null\n });\n sendJson(res, 202, { ok: true, reportId: entry.id, status: entry.status });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsModerationQueue) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n queue: listModerationQueueV0(100),\n softBlocked: listSoftBlockedUsersV0()\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-block\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const out = softBlockUserV0(body?.uid, body?.reason);\n sendJson(res, out.ok ? 200 : 400, out);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-unblock\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n sendJson(res, 200, removeSoftBlockV0(body?.uid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsAgentSnapshots) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n const limit = Number(new URL(req.url || \"\", \"http://local\").searchParams.get(\"limit\") || 50);\n sendJson(res, 200, { ok: true, snapshots: listRecentAgentSnapshotsV0(limit) });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.academicObservatoryExport) {\n const auth = await resolveHttpUser(req);\n const out = handleAcademicObservatoryExportGetV0(req, auth);\n sendJson(res, out.status, out.body);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsHardeningStatus) {\n const auth = await resolveHttpUser(req);\n const principal = auth.ok ? `uid:${auth.uid}` : null;\n const gclSnap = await getGlobalCostLedgerSnapshotV0(principal);\n const economicStrategy = evaluateEconomicStrategyL3V0(gclSnap);\n const hardeningTenantId = resolveHardeningTenantIdV0(auth, req);\n const unifiedState = await buildUnifiedStateNarrativeV0({\n tenantId: hardeningTenantId ?? undefined,\n platformScope: !hardeningTenantId,\n principal\n });\n sendJson(res, 200, {\n ok: true,\n schema: \"rhizoh.ops.hardening_status.v0\",\n containment: readAgentContainmentConfigV0(),\n cost: readCostContainmentConfigV0(),\n gcl: {\n backend: await getGclBackendStatusV0(),\n snapshot: gclSnap\n },\n economicStrategyL3: economicStrategy,\n phasedRollout: await getPhasedRolloutStatsV0(),\n tier: readPhasedRolloutTierV0(),\n principalCost: auth.ok ? await getCostContainmentStatsV0(principal) : null,\n stressTaxonomy: verifyStressTaxonomyCoverageV0(),\n stressConflictResolution: verifyStressConflictResolutionV0(),\n resolutionStabilityEnvelope: verifyResolutionStabilityEnvelopeV0(),\n unifiedState: {\n tenantScope: {\n tenantId: unifiedState.tenantScope?.tenantId,\n scope: unifiedState.tenantScope?.scope,\n noGlobalDerivedState: unifiedState.tenantScope?.noGlobalDerivedState\n },\n narrativeFingerprint: unifiedState.narrativeFingerprint,\n screenshotScopeWatermark: unifiedState.screenshotScopeWatermark,\n interpretationSafetyContract: unifiedState.interpretationSafetyContract,\n interpretationUxContract: {\n schema: unifiedState.interpretationUxContract?.schema,\n layerOrder: unifiedState.interpretationUxContract?.layerOrder,\n narrativeNeverVisuallyDominant: unifiedState.interpretationUxContract?.narrativeNeverVisuallyDominant\n },\n governance: {\n narrativeIsNotDecisionLayer: unifiedState.governance.narrativeIsNotDecisionLayer,\n displayOrder: unifiedState.governance.displayOrder,\n trustPosture: unifiedState.governance.trustPosture,\n interpretationUxContract: unifiedState.governance.interpretationUxContract?.schema\n },\n humanOps: {\n decisionTier: unifiedState.humanOps?.humanDecisionScaling?.tier,\n decisionOwner: unifiedState.humanOps?.humanDecisionScaling?.decisionOwner,\n opsRouteId: unifiedState.humanOps?.humanDecisionOpsRunbook?.routing?.routeId,\n slaAckMinutes: unifiedState.humanOps?.humanDecisionOpsRunbook?.sla?.ackMinutes,\n uiDisclaimerRequired: unifiedState.humanOps?.narrativeUiSafety?.displayRules?.requireDisclaimerBanner,\n misreadHighResidual: unifiedState.humanOps?.misreadSimulation?.highResidualCount,\n propagationHighResidual: unifiedState.humanOps?.socialPropagationSimulation?.highResidualCount,\n worstWatermarkSurvivability:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.worstWatermarkSurvivability,\n dominantDistortionSource:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.dominantDistortionSource\n },\n culturalRisk: {\n perceptualRiskClass: unifiedState.culturalRisk?.perceptualRiskClass,\n trustFork: unifiedState.culturalRisk?.trustDynamics?.fork,\n trustDecayScore: unifiedState.culturalRisk?.trustDynamics?.scores?.trustDecay,\n mythologyScore: unifiedState.culturalRisk?.trustDynamics?.scores?.mythology,\n confidenceDetachmentExposure:\n unifiedState.culturalRisk?.trustDynamics?.axes?.confidenceDetachmentExposure\n },\n appliedSystemsLayer: {\n schema: unifiedState.appliedSystemsLayer?.schema,\n researchOutputType: unifiedState.appliedSystemsLayer?.researchGovernance?.outputType,\n roboticsBlockActuation: unifiedState.appliedSystemsLayer?.roboticsGrounding?.blockActuation,\n spiralRumorCount:\n unifiedState.appliedSystemsLayer?.spiralMMOGameKernel?.gameLayer?.rumors?.length\n },\n actionSemanticGovernance: {\n schema: unifiedState.actionSemanticGovernance?.schema,\n highRiskActionCount: unifiedState.actionSemanticGovernance?.highRiskActionCount,\n loopClosed: unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.loopClosed,\n authorityCollapseMitigated:\n unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.risks?.authorityCollapse\n ?.mitigated\n },\n actionContextResolution: {\n schema: unifiedState.actionContextResolution?.schema,\n contextFingerprint: unifiedState.actionContextResolution?.contextFingerprint?.fingerprintHash,\n domainCollision: unifiedState.actionContextResolution?.domainCollision?.detected,\n deployAgentSpiralBlocked:\n unifiedState.actionContextResolution?.actionOverloadingRisk?.deployAgentExample\n ?.spiral_mmo === \"blocked\",\n roboticsActuationEligible:\n unifiedState.actionContextResolution?.roboticsActuationCorrectness?.actuationEligible\n },\n epistemicCoherence: {\n schema: unifiedState.epistemicCoherence?.schema,\n verdict: unifiedState.epistemicCoherence?.systemCoherence?.verdict,\n coherenceScore: unifiedState.epistemicCoherence?.systemCoherence?.score,\n contradictionCount: unifiedState.epistemicCoherence?.crossLayerContradictions?.count,\n operatorDecision: unifiedState.epistemicCoherence?.uxCompression?.operatorDecision,\n headlineTr: unifiedState.epistemicCoherence?.uxCompression?.headline?.tr,\n falseSafetyWarning: Boolean(\n unifiedState.epistemicCoherence?.uxCompression?.falseSafetyWarning\n )\n },\n coherenceAuthorityBoundary: {\n schema: unifiedState.coherenceAuthorityBoundary?.schema,\n bindingValid: unifiedState.coherenceAuthorityBoundary?.eclAuthorityBinding?.valid,\n canExecute: unifiedState.coherenceAuthorityBoundary?.contract?.can_execute,\n mandatoryBannerTr:\n unifiedState.coherenceAuthorityBoundary?.disclaimers?.mandatoryBanner?.tr\n },\n decisionLatencyGovernance: {\n schema: unifiedState.decisionLatencyGovernance?.schema,\n latencyTier: unifiedState.decisionLatencyGovernance?.latencyTier,\n inflationScore: unifiedState.decisionLatencyGovernance?.latencyInflation?.score,\n fastPathEligible: unifiedState.decisionLatencyGovernance?.fastPath?.eligible,\n primaryActionId: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.primaryActionId,\n ackSlaMinutes: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.ackSlaMinutes,\n certaintyCap: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.certaintyCap,\n uncertaintyItems:\n unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.uncertaintyEnvelope?.itemCount\n },\n decisionPacketUncertaintyBoundary: {\n schema: unifiedState.decisionPacketUncertaintyBoundary?.schema,\n invariantValid: unifiedState.decisionPacketUncertaintyBoundary?.invariantCheck?.valid,\n centralInvariant: unifiedState.decisionPacketUncertaintyBoundary?.centralInvariant\n },\n epistemicDecisionPacing: {\n schema: unifiedState.epistemicDecisionPacing?.schema,\n queueSaturationTier:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation?.tier,\n operatorProcessingLatency:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation\n ?.operatorProcessingLatency,\n postureWindowCount:\n unifiedState.epistemicDecisionPacing?.temporaryStaticPostureWindows\n ?.temporaryStaticPostureWindows?.windowCount,\n executionBoundariesValid:\n unifiedState.epistemicDecisionPacing?.executionBoundaries?.valid,\n grantsExecutionApproval: unifiedState.epistemicDecisionPacing?.grantsExecutionApproval\n },\n epistemicTemporalCoherence: {\n schema: unifiedState.epistemicTemporalCoherence?.schema,\n allInvariantsValid: unifiedState.epistemicTemporalCoherence?.allInvariantsValid,\n temporalAlignmentValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.temporalAlignment?.valid,\n crossWindowGuardValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard?.valid,\n delayedTruthValid: unifiedState.epistemicTemporalCoherence?.invariants?.delayedTruthRule?.valid,\n reconciliationRequired:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard\n ?.reconciliation?.required\n },\n realityDriftObserver: {\n schema: unifiedState.realityDriftObserver?.schema,\n driftActive: unifiedState.realityDriftObserver?.driftObserverSummary?.active,\n misapprehensionShapeCount:\n unifiedState.realityDriftObserver?.misapprehensionShapeCatalog?.shapeCount,\n criticalShapeCount:\n unifiedState.realityDriftObserver?.driftObserverSummary?.criticalShapeCount,\n roboticsMismatchActive:\n unifiedState.realityDriftObserver?.roboticsFeedbackMismatch?.active\n },\n driftCausality: {\n schema: unifiedState.driftCausality?.schema,\n shapesWithCausalExplanation: unifiedState.driftCausality?.rdolLinkage?.shapesWithCausalExplanation,\n activeDomainCount: unifiedState.driftCausality?.causalitySummary?.activeDomainCount,\n propagationCausalityActive: unifiedState.driftCausality?.causalDomains?.propagation?.active\n },\n productReadiness: unifiedState.productReadiness,\n stateLayers: {\n raw: unifiedState.stateLayers.raw,\n derived: {\n systemState: unifiedState.stateLayers.derived.systemState,\n narrative: unifiedState.stateLayers.derived.narrative,\n validation: unifiedState.stateLayers.derived.validation\n },\n policy: {\n binding: unifiedState.stateLayers.policy.binding,\n decisionOwner: unifiedState.stateLayers.policy.decisionOwner,\n prohibitedActions: unifiedState.stateLayers.policy.prohibitedActions\n }\n },\n systemState: unifiedState.systemState,\n interpretation: unifiedState.interpretation,\n validation: {\n trustPosture: unifiedState.validation.trustPosture,\n adjustedConfidence: unifiedState.validation.adjustedConfidence,\n agreement: unifiedState.validation.confidenceDecomposition.agreement,\n divergenceScore: unifiedState.validation.divergence.divergenceScore,\n validated: unifiedState.validation.divergence.validated,\n uncertaintyTags: unifiedState.validation.uncertainty.tags.map((t) => t.id),\n flags: unifiedState.validation.divergence.flags.map((f) => f.id)\n },\n compression: unifiedState.compression\n }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 6 * 1024 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (\n !checkHttpRateLimit(`rhizoh_voice_transcribe:${rlKey}`, RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN, 60_000)\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const voiceEnv = rhizohVoiceTranscribeEnvV3();\n if (!voiceEnv.any) {\n sendJson(res, 503, {\n ok: false,\n error: \"voice_asr_not_configured\",\n env: voiceEnv\n });\n return;\n }\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n const result = await runRhizohVoiceTranscribeV3(\n applyLanguagePropagationToVoiceBodyV1(payload, langProp)\n );\n sendJson(res, result.ok ? 200 : 422, {\n ...result,\n traceId: String(payload?.traceId || \"\"),\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n atMs: Date.now()\n });\n } catch (error) {\n sendJson(res, 500, {\n ok: false,\n error: \"voice_transcribe_failed\",\n detail: String(error?.message || error),\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === rhizohRuntime.routes.rhizohLlm) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 128 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n logRhizohHealth(\"gateway_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: String(payload?.traceId || \"\"),\n auth: auth.ok ? \"ok\" : \"anon\",\n ip\n });\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (!checkHttpRateLimit(`rhizoh_llm:${rlKey}`, RL_RHIZOH_LLM_PER_MIN, 60_000)) {\n const rlTaxonomy = classifyStressResponseV0({ code: \"rate_limit_exceeded\" });\n return sendJson(res, 429, {\n ok: false,\n error: \"rate_limit_exceeded\",\n reply: rlTaxonomy.userMessageTr || \"İstek sınırı aşıldı. Kısa süre sonra tekrar deneyin.\",\n stressClass: rlTaxonomy.stressClass,\n responseAction: rlTaxonomy.responseAction,\n stressMatrix: rlTaxonomy.matrix,\n stressInterpretable: rlTaxonomy.interpretable,\n stressConfidence: rlTaxonomy.stressConfidence,\n actionConfidence: rlTaxonomy.actionConfidence,\n actionSoftened: rlTaxonomy.actionSoftened,\n responseActionStrict: rlTaxonomy.responseActionStrict,\n actionInterpretable: rlTaxonomy.actionInterpretable,\n conflictResolution: rlTaxonomy.conflictResolution,\n directive: \"NONE\"\n });\n }\n\n const requireExplicit = process.env.CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE === \"1\";\n const keyMode = normalizeClientLlmKeySource(payload?.llmKeySource ?? payload?.keySource);\n if (keyMode === null) {\n return sendJson(res, 400, {\n ok: false,\n error: \"invalid_llm_key_source\",\n reply: \"llmKeySource geçersiz. Kullanın: env | user_connection | auto\",\n directive: \"NONE\"\n });\n }\n if (requireExplicit && keyMode === \"auto\") {\n return sendJson(res, 400, {\n ok: false,\n error: \"llm_key_source_required\",\n reply: \"Sunucu CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE=1: istekte llmKeySource (env veya user_connection) zorunlu.\",\n directive: \"NONE\"\n });\n }\n\n const conn = auth.ok ? await resolveConnection(auth.uid, String(payload?.connectionId || \"\")) : null;\n\n if (keyMode === \"user_connection\") {\n if (!auth.ok) {\n return sendJson(res, 401, {\n ok: false,\n error: \"llm_auth_required_for_user_connection\",\n reply: \"Kayıtlı LLM bağlantısı için giriş gerekli.\",\n directive: \"NONE\"\n });\n }\n if (!conn?.apiKey) {\n return sendJson(res, 400, {\n ok: false,\n error: \"user_llm_connection_required\",\n reply: \"Geçerli bir LLM bağlantısı yok; Studio’da bağlantı ekleyin veya llmKeySource=env kullanın.\",\n directive: \"NONE\"\n });\n }\n }\n\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n let safePayload = applyLanguagePropagationToLlmPayloadV1({ ...(payload || {}) }, langProp);\n const trimOut = trimRhizohLlmGatewayPayloadV1(safePayload);\n if (trimOut.meta.trimmed) {\n logRhizohHealth(\"llm_payload_trimmed\", {\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n bytesBefore: trimOut.meta.bytesBefore,\n bytesAfter: trimOut.meta.bytesAfter\n });\n }\n safePayload = trimOut.payload;\n delete safePayload.apiKey;\n delete safePayload.llmKeySource;\n delete safePayload.keySource;\n\n let resolvedProvider = payload?.provider;\n let resolvedModel = payload?.model;\n let connApiKey = \"\";\n\n if (keyMode === \"user_connection\") {\n resolvedProvider = conn.provider;\n resolvedModel = conn.model;\n connApiKey = conn.apiKey || \"\";\n } else if (keyMode === \"env\") {\n connApiKey = \"\";\n } else {\n resolvedProvider = conn?.provider || payload?.provider;\n resolvedModel = conn?.model || payload?.model;\n connApiKey = conn?.apiKey || \"\";\n }\n\n const { result, traceId, turnLatencyMs, spinePhases, sampledTrace } = await rhizohGatewayTurn({\n safePayload,\n auth,\n keyMode,\n conn,\n resolvedProvider,\n resolvedModel,\n connApiKey\n });\n logRhizohHealth(\"llm_response\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId,\n turnLatencyMs: Number(turnLatencyMs || 0),\n keyMode\n });\n sendJson(res, 200, {\n ...result,\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n traceId,\n turnLatencyMs,\n ...(spinePhases ? { spinePhases } : {}),\n ...(sampledTrace !== undefined ? { sampledTrace } : {})\n });\n } catch (error) {\n const msg = String(error?.message || \"\");\n const code = error?.code || \"\";\n let status = 500;\n if (msg === \"payload_too_large\") status = 413;\n else if (msg === \"rate_limit_exceeded\") status = 429;\n else if (\n code === \"cost_hard_limit\" ||\n code === \"phased_rollout_capacity\" ||\n error?.containment === true ||\n String(code).startsWith(\"agent_\")\n ) {\n status = 429;\n } else if (code === \"user_soft_blocked\" || code === \"prompt_abuse_detected\") status = 403;\n else if ([\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(code) || msg.includes(\"missing_api_key_for_\"))\n status = 400;\n let rhizohFailureKind = \"provider_error\";\n if (msg === \"rate_limit_exceeded\" || status === 429) rhizohFailureKind = \"rate_limit\";\n else if (status === 403) rhizohFailureKind = \"policy_block\";\n else if (status === 400 || status === 413) rhizohFailureKind = \"client_config\";\n else if (msg.startsWith(\"provider_http_\")) {\n const st = Number(msg.replace(\"provider_http_\", \"\"));\n if (st === 408 || st === 504) rhizohFailureKind = \"timeout\";\n else rhizohFailureKind = \"provider_error\";\n } else if (/timeout|timed out|ETIMEDOUT/i.test(msg)) rhizohFailureKind = \"timeout\";\n\n const providerHttpMatch = msg.match(/^provider_http_(\\d{3})$/);\n const stressTaxonomy =\n error?.stressClass != null\n ? {\n stressClass: error.stressClass,\n responseAction: error.responseAction,\n stressMatrix: error.stressMatrix,\n stressInterpretable: error.stressInterpretable !== false,\n stressConfidence: error.stressConfidence,\n actionConfidence: error.actionConfidence,\n actionSoftened: error.actionSoftened,\n responseActionStrict: error.responseActionStrict,\n actionInterpretable: error.actionInterpretable,\n conflictResolution: error.conflictResolution,\n stressSecondary: error.stressSecondary,\n responseActions: error.responseActions\n }\n : (() => {\n const t = classifyStressResponseV0({\n code: code || msg,\n rhizohFailureKind,\n providerHttpStatus: providerHttpMatch\n ? Number(providerHttpMatch[1])\n : null\n });\n return {\n stressClass: t.stressClass,\n responseAction: t.responseAction,\n stressMatrix: t.matrix,\n stressInterpretable: t.interpretable,\n stressConfidence: t.stressConfidence,\n actionConfidence: t.actionConfidence,\n actionSoftened: t.actionSoftened,\n responseActionStrict: t.responseActionStrict,\n actionInterpretable: t.actionInterpretable,\n conflictResolution: t.conflictResolution,\n stressSecondary: t.stressSecondary,\n responseActions: t.responseActions\n };\n })();\n sendJson(res, status, {\n ok: false,\n rhizohFailureKind,\n ...(providerHttpMatch ? { providerHttpStatus: Number(providerHttpMatch[1]) } : {}),\n error: code || msg || \"rhizoh_llm_failed\",\n detail: msg || code || undefined,\n reply:\n error?.reply ||\n (status === 500 ? \"Rhizoh bağlantısı geçici olarak kesildi.\" : msg),\n directive: error?.directive || \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n ...stressTaxonomy\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicSeal) {\n try {\n if (!EPISTEMIC_SEAL_SECRET) {\n sendJson(res, 503, { ok: false, error: \"epistemic_seal_disabled\" });\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`epistemic_seal:${ip}`, RL_EPISTEMIC_SEAL_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const truth = body.truth_contract;\n if (!truth || typeof truth !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"truth_contract_required\" });\n return;\n }\n const sealBody = {\n truth_contract: truth,\n runtime_hash: String(body.runtime_hash || \"\"),\n model_route: {\n provider: body.model_route?.provider ?? null,\n model: body.model_route?.model ?? null\n },\n memory_digest: String(body.memory_digest || \"\"),\n world_snapshot_hash: String(body.world_snapshot_hash || \"\"),\n timestamp: Number(body.timestamp) || Date.now()\n };\n const canonical = canonicalEpistemicSealString(sealBody);\n const { hash, signature } = hashAndSignEpistemicSeal(canonical, EPISTEMIC_SEAL_SECRET);\n recordGenesisEpistemicSealIssued(hash);\n const attestation = {\n schema: EPISTEMIC_SEAL_SCHEMA,\n algorithm: \"SHA-256+HMAC-SHA256\",\n keyHint: process.env.CASTLE_EPISTEMIC_SEAL_SECRET ? \"CASTLE_EPISTEMIC_SEAL_SECRET\" : \"CASTLE_GATEWAY_TOKEN\",\n issuedAt: Date.now(),\n gateway: { service: \"castle-gateway\", listenPort: PORT }\n };\n sendJson(res, 200, {\n ok: true,\n hash,\n signature,\n attestation,\n canonicalBytes: Buffer.byteLength(canonical, \"utf8\")\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"seal_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicLogsBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`epistemic_logs:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 120) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const persisted = await persistEpistemicLedgerBatch(auth.uid, entries);\n recordGenesisEpistemicLedgerPersisted(persisted.persisted);\n // Internal cognition layer: forecast stays gateway/ledger-only (no client exposure).\n try {\n await persistEpistemicForecastBatch(auth.uid, persisted.normalized || []);\n } catch {\n /* forecast failures must not break primary ledger ingest */\n }\n sendJson(res, 200, {\n ok: true,\n persisted: persisted.persisted,\n mode: persisted.mode,\n latest: persisted.normalized?.[persisted.normalized.length - 1] || null\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"epistemic_log_ingest_failed\") });\n }\n return;\n }\n\n sendJson(res, 404, { ok: false, error: \"not_found\" });\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\nspiralWssForGenesis = wss;\nconst orchestrator = createOrchestrator();\nconst clientStats = new Map();\nlet broadcasterClientId = null;\nconst spiralState = {\n rooms: [{ id: \"room-default\", name: \"Spiral Default Arena\" }],\n activeByClient: new Map(),\n characters: new Map() // id -> {id,name,role,roomId,source}\n};\n\n/** Castle social rooms — keyed by `castleRoomKey` from {@link WS_MESSAGE.CASTLE_SOCIAL_PULSE}. */\nconst castleSocialRoomState = new Map();\nconst CASTLE_SOCIAL_MEMBER_TTL_MS = 90_000;\nconst CASTLE_SOCIAL_MAX_PULSE_BYTES = 8192;\n\nfunction removeCastleSocialClientFromAllRooms(clientId) {\n const cid = String(clientId || \"\");\n if (!cid) return;\n for (const room of castleSocialRoomState.values()) {\n room.members.delete(cid);\n }\n}\n\nfunction buildCastleSocialRoomPayload(roomKey) {\n const room = castleSocialRoomState.get(roomKey);\n if (!room) return null;\n const now = Date.now();\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const roster = [...room.members.values()].map((m) => ({\n userId: m.userId,\n lastMs: m.lastMs,\n ...m.sp\n }));\n const peerFeeds =\n room.walPeers && room.walPeers.size\n ? [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }))\n : [];\n return { castleRoomKey: roomKey, seq: room.seq, roster, peerFeeds, ts: now };\n}\n\nconst CASTLE_WAL_PEER_MAX_FEED_BYTES = 48 * 1024;\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {{ ok: boolean, code?: string, reason?: string }} gate\n */\nfunction rejectWalPeerIngressV0(socket, gate) {\n const code = gate.code || \"wal_peer_rejected\";\n recordWalPeerRejectV0(code);\n recordSubstrateOtelEventV0(\"wal_peer_reject\", { code, reason: String(gate.reason || \"\").slice(0, 120) });\n socket.send(\n JSON.stringify(\n createEnvelope(WS_MESSAGE.ERROR, {\n error: gate.reason || \"WAL peer ingress rejected.\",\n code\n })\n )\n );\n}\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {Record<string, unknown> | null | undefined} walPeerFeed\n * @returns {boolean}\n */\nfunction gateWalPeerIngressV0(socket, walPeerFeed) {\n const authGate = assertWalPeerSocketAuthorityV0(socket);\n if (!authGate.ok) {\n rejectWalPeerIngressV0(socket, authGate);\n return false;\n }\n const sigGate = validateWalPeerFeedSignatureV0(\n walPeerFeed && typeof walPeerFeed === \"object\" ? walPeerFeed : {}\n );\n if (!sigGate.ok) {\n rejectWalPeerIngressV0(socket, sigGate);\n return false;\n }\n return true;\n}\n\nfunction handleCastleWalPeerFeed(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_WAL_PEER_MAX_FEED_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_WAL_PEER_FEED too large.\" })));\n return;\n }\n const walPeerFeed = p.walPeerFeed && typeof p.walPeerFeed === \"object\" ? p.walPeerFeed : {};\n if (!gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const castleRoomKey = String(p.castleRoomKey || \"wal:convergence\").slice(0, 64);\n const castleId = String(p.castleId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId,\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: room.members.get(socket.clientId)?.sp ?? {}\n });\n const peerFeeds = [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }));\n const body = {\n castleRoomKey,\n seq: room.seq,\n peerFeeds,\n ts: now\n };\n const env = createEnvelope(WS_MESSAGE.CASTLE_WAL_PEER_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys(), ...room.walPeers.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction handleCastleSocialPulse(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_SOCIAL_MAX_PULSE_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_SOCIAL_PULSE too large.\" })));\n return;\n }\n const castleRoomKey = String(p.castleRoomKey || \"default\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n const sp = p.socialPulse && typeof p.socialPulse === \"object\" ? p.socialPulse : {};\n const walPeerFeed =\n p.walPeerFeed && typeof p.walPeerFeed === \"object\"\n ? p.walPeerFeed\n : sp.walPeerFeed && typeof sp.walPeerFeed === \"object\"\n ? sp.walPeerFeed\n : null;\n if (walPeerFeed && !gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const spNext = walPeerFeed ? { ...sp, walPeerFeed } : { ...sp };\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: spNext\n });\n if (walPeerFeed) {\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId: String(p.castleId || userId).slice(0, 64),\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n }\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const body = buildCastleSocialRoomPayload(castleRoomKey);\n if (!body) return;\n const env = createEnvelope(WS_MESSAGE.CASTLE_SOCIAL_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcast(msgObj) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcastState() {\n broadcast(\n createEnvelope(WS_MESSAGE.BROADCAST_STATE, {\n broadcasterClientId,\n viewers: [...wss.clients].filter((c) => c.readyState === 1).length\n })\n );\n}\n\nfunction currentSpiralStatePayload() {\n return {\n rooms: spiralState.rooms,\n characters: [...spiralState.characters.values()]\n };\n}\n\nfunction broadcastSpiralState() {\n broadcast(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload()));\n}\n\nfunction ensureRoom(roomId, fallbackName = null) {\n if (!roomId) return spiralState.rooms[0].id;\n const exists = spiralState.rooms.find((r) => r.id === roomId);\n if (exists) return exists.id;\n const created = { id: roomId, name: fallbackName || `Spiral-${roomId.slice(-4)}` };\n spiralState.rooms.push(created);\n return created.id;\n}\n\nfunction syncSpiralCharactersWithWorld(snapshot) {\n const agents = snapshot?.agents || [];\n for (const a of agents) {\n if (!spiralState.characters.has(a.id)) {\n spiralState.characters.set(a.id, {\n id: a.id,\n name: a.id,\n role: \"agent\",\n roomId: spiralState.rooms[0].id,\n source: \"world\"\n });\n }\n }\n}\n\nfunction broadcastExcept(msgObj, exceptSocket) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client !== exceptSocket && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction validateInputFrame(payload, socket) {\n const stats = clientStats.get(socket.clientId) || { lastFrame: 0, windowStart: Date.now(), msgCount: 0 };\n const now = Date.now();\n if (now - stats.windowStart > 5000) {\n stats.windowStart = now;\n stats.msgCount = 0;\n }\n stats.msgCount += 1;\n clientStats.set(socket.clientId, stats);\n if (stats.msgCount > 120) return \"Rate limit exceeded.\";\n\n if (!payload || !Number.isInteger(payload.frame)) return \"frame must be integer.\";\n if (payload.frame <= stats.lastFrame) return \"frame must be monotonic.\";\n const commands = Array.isArray(payload.commands) ? payload.commands : [];\n if (commands.length > 8) return \"Too many commands in one frame.\";\n for (const cmd of commands) {\n if (!cmd || typeof cmd.command !== \"string\") return \"Invalid command format.\";\n if (\n cmd.command !== \"SPAWN_AGENT\" &&\n cmd.command !== \"LIST_CASTLES\" &&\n cmd.command !== \"SPAWN_ENTITY\" &&\n cmd.command !== \"CREATE_CASTLE\"\n ) return \"Unsupported command.\";\n }\n stats.lastFrame = payload.frame;\n clientStats.set(socket.clientId, stats);\n return null;\n}\n\nfunction validateSignalPayload(payload) {\n if (!payload || typeof payload !== \"object\") return \"Invalid signaling payload.\";\n const signalType = payload.signalType;\n if (![\"OFFER\", \"ANSWER\", \"ICE\"].includes(signalType)) return \"Invalid signal type.\";\n if (typeof payload.to !== \"string\" || payload.to.length < 3) return \"Invalid signal destination.\";\n if (signalType !== \"ICE\" && JSON.stringify(payload.sdp || {}).length > 16 * 1024) return \"SDP too large.\";\n if (signalType === \"ICE\" && JSON.stringify(payload.candidate || {}).length > 6 * 1024) return \"ICE too large.\";\n return null;\n}\n\nfunction sanitizeRawMessage(raw) {\n if (!raw) return null;\n const text = raw.toString();\n if (Buffer.byteLength(text, \"utf8\") > MAX_MESSAGE_BYTES) return null;\n return text;\n}\n\nwss.on(\"connection\", async (socket, req) => {\n const origin = req?.headers?.origin || \"\";\n if (!httpCors.isWebSocketOriginAllowed(origin)) {\n socket.close(1008, \"Origin not allowed\");\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const path = req?.url || \"\";\n const query = new URL(`ws://localhost${path}`).searchParams;\n const token = query.get(\"token\");\n if (!token || token !== REQUIRED_GATEWAY_TOKEN) {\n socket.close(1008, \"Invalid gateway token\");\n return;\n }\n }\n if (REQUIRE_AUTH) {\n const result = await verifyClientToken(req);\n if (!result.ok) {\n const isLocal = origin.includes(\"localhost\") || origin.includes(\"127.0.0.1\") || !origin;\n if (!(ALLOW_DEV_ANON && isLocal)) {\n socket.close(1008, result.reason);\n return;\n }\n socket.auth = { ok: true, kind: \"dev-anon\", user: { uid: \"dev-anon\" } };\n } else {\n socket.auth = result;\n }\n }\n\n socket.clientId = `c-${Math.random().toString(36).slice(2, 8)}`;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.HELLO, { message: \"Castle Gateway online.\", clientId: socket.clientId })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.WORLD_SNAPSHOT, orchestrator.worldSnapshot())));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.BROADCAST_STATE, { broadcasterClientId, viewers: [...wss.clients].filter((c) => c.readyState === 1).length })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload())));\n broadcastExcept(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }), socket);\n\n socket.on(\"message\", (raw) => {\n const safeRaw = sanitizeRawMessage(raw);\n if (!safeRaw) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Message too large or invalid.\" })));\n return;\n }\n const parsed = safeJsonParse(safeRaw);\n if (!parsed?.type) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid message.\" })));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND_TEXT) {\n const canonical = parseTextToCommand(parsed.payload?.text);\n if (!canonical) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Unknown command text.\" })));\n return;\n }\n const result = orchestrator.applyCommand(canonical);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND) {\n const result = orchestrator.applyCommand(parsed.payload);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n }\n\n if (parsed.type === WS_MESSAGE.INPUT_FRAME) {\n const err = validateInputFrame(parsed.payload, socket);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const frame = parsed.payload.frame;\n const commands = Array.isArray(parsed.payload?.commands) ? parsed.payload.commands : [];\n orchestrator.queueInputFrame(socket.clientId, frame, commands);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, queued: true, frame })));\n }\n\n if (parsed.type === WS_MESSAGE.SIGNAL) {\n const payload = parsed.payload || {};\n const err = validateSignalPayload(payload);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const signalType = payload.signalType;\n const to = payload.to;\n for (const client of wss.clients) {\n if (client.clientId === to && client.readyState === 1) {\n client.send(JSON.stringify(createEnvelope(WS_MESSAGE.SIGNAL, { ...payload, from: socket.clientId })));\n break;\n }\n }\n }\n\n if (parsed.type === WS_MESSAGE.DRONE_TELEMETRY) {\n const payload = parsed.payload || {};\n const json = JSON.stringify(payload);\n if (Buffer.byteLength(json, \"utf8\") > 48 * 1024) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"DRONE_TELEMETRY payload too large.\" })));\n return;\n }\n broadcastExcept(createEnvelope(WS_MESSAGE.DRONE_TELEMETRY, { ...payload, relayFrom: socket.clientId }), socket);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_SOCIAL_PULSE) {\n handleCastleSocialPulse(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_WAL_PEER_FEED) {\n handleCastleWalPeerFeed(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.OPEN_DATA_QUERY) {\n queryOpenData(parsed.payload)\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: true, result })));\n })\n .catch((e) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: false, error: e.message })));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_QUERY) {\n runRhizohBrainV2({\n ...(parsed.payload || {}),\n uid: socket?.auth?.user?.uid || \"unknown\"\n })\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, result)));\n })\n .catch(async () => {\n const fallback = await runRhizohBrain(parsed.payload || {}).catch(() => ({\n ok: false,\n reply: \"Rhizoh temporary disturbance. Habitat remains stable.\"\n }));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, fallback)));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.BROADCAST_REGISTER) {\n const role = String(parsed.payload?.role || \"\");\n if (role === \"GENESIS_BROADCAST_AGENT\") {\n broadcasterClientId = socket.clientId;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, registered: role })));\n broadcastState();\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.STUDIO_CUE) {\n // Only registered broadcaster can push global cues.\n if (!broadcasterClientId || socket.clientId !== broadcasterClientId) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Only broadcaster agent can emit STUDIO_CUE.\" })));\n return;\n }\n const cue = parsed.payload || {};\n if (typeof cue !== \"object\") {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid STUDIO_CUE payload.\" })));\n return;\n }\n broadcast(createEnvelope(WS_MESSAGE.STUDIO_CUE, cue));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_JOIN_ROOM) {\n const requested = String(parsed.payload?.roomId || \"\").trim();\n const requestedName = String(parsed.payload?.roomName || \"\").trim();\n const roomId = ensureRoom(requested || `room-${Math.random().toString(36).slice(2, 7)}`, requestedName || null);\n spiralState.activeByClient.set(socket.clientId, roomId);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, roomId })));\n broadcastSpiralState();\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_CREATE_CHARACTER) {\n const name = String(parsed.payload?.name || \"\").trim().slice(0, 32) || `AI-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;\n const role = String(parsed.payload?.role || \"agent\").trim().slice(0, 24) || \"agent\";\n const roomId = ensureRoom(\n String(parsed.payload?.roomId || spiralState.activeByClient.get(socket.clientId) || spiralState.rooms[0].id),\n null\n );\n\n // Character creation always maps to an authoritative world agent.\n const result = orchestrator.applyCommand({\n command: COMMAND.SPAWN_AGENT,\n params: { targetCastleId: \"castle-01\" }\n });\n if (!result?.ok || !result?.data?.id) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Character creation failed.\" })));\n return;\n }\n const id = result.data.id;\n spiralState.characters.set(id, { id, name, role, roomId, source: \"spawned\" });\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, characterId: id, roomId })));\n broadcastSpiralState();\n return;\n }\n });\n\n socket.on(\"close\", () => {\n if (socket.clientId === broadcasterClientId) broadcasterClientId = null;\n clientStats.delete(socket.clientId);\n spiralState.activeByClient.delete(socket.clientId);\n removeCastleSocialClientFromAllRooms(socket.clientId);\n broadcast(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }));\n broadcastState();\n });\n});\n\nsetInterval(() => {\n void (async () => {\n try {\n const { frame, snapshot, delta, packedDelta } = orchestrator.tick(1 / 20);\n syncSpiralCharactersWithWorld(snapshot);\n broadcast(createEnvelope(WS_MESSAGE.NETWORK_TICK, { frame }));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_TICK, snapshot));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA, delta));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA_PACKED, packedDelta));\n broadcastSpiralState();\n await runAcademyEventTick();\n const executed = await tickAutomations(Date.now());\n for (const a of executed) {\n await addTranscript(a.uid, {\n source: \"planner\",\n eventType: \"automation-executed\",\n text: `Automation executed: ${a.title} (${a.action})`,\n roomId: \"studio-main\",\n meta: { automationId: a.id, action: a.action, deviceId: a.deviceId || \"\" }\n });\n }\n } catch (e) {\n console.error(\"[GATEWAY] world tick / automations loop error:\", String(e?.message || e));\n }\n })();\n}, 50);\n\n(async () => {\n await rhizohRuntime.telemetry.initOpenTelemetry();\n if (String(process.env.CASTLE_GENESIS_DISK_PERSIST ?? \"\").trim() === \"\") {\n process.env.CASTLE_GENESIS_DISK_PERSIST = \"1\";\n }\n const genesisDiskBoot = await hydrateGenesisContinuityPersistenceBootV0();\n if (!genesisDiskBoot.ok) {\n console.warn(\"[GATEWAY] genesis disk hydrate failed; continuing with in-memory genesis continuity\");\n }\n httpServer.listen(PORT, () => {\n startGenesisCanonicalClock();\n installGenesisCheckpointSurfaceGetter(buildGenesisRuntimeSurfacePayloadLive);\n setGenesisContinuityAfterPublishHook((seq) => {\n noteGenesisCheckpointSeqCommitted(seq);\n noteGenesisContinuitySeqAcceptedV0(seq);\n });\n startGenesisContinuityInfraSampler(buildGenesisRuntimeSurfacePayloadLive, 2000);\n logProductionObservatorySurfaceGuardsV0();\n logGatewaySubstrateAuthorityGuardsV0();\n console.log(`[GATEWAY] ws/http://localhost:${PORT}`);\n console.log(\n `[GATEWAY] genesis continuity: GET ${rhizohRuntime.routes.genesisRuntime} | POST ${rhizohRuntime.routes.genesisIngress} | SSE ${rhizohRuntime.routes.genesisStream} | replay ${rhizohRuntime.routes.genesisReplay} | diff ${rhizohRuntime.routes.genesisReplayDiff} | equiv ${rhizohRuntime.routes.genesisReplayEquivalence} | analytics ${rhizohRuntime.routes.genesisReplayAnalytics} | evolution ${rhizohRuntime.routes.genesisReplayEvolution}`\n );\n if (String(process.env.CASTLE_GENESIS_OBSERVATORY_LIVE_V01 ?? \"\").trim() === \"1\") {\n console.log(\"GENESIS OBSERVATORY LIVE v0.1 — READ ONLY PHASE STARTED\");\n }\n });\n})().catch((e) => {\n console.error(\"[GATEWAY] fatal boot (before or during listen):\", e);\n process.exit(1);\n});\n"}, "d736521cef7620f1fbf3afa2e1bcf1bdb2b1847d": {"t": 1785754158.4216611, "n": 155472, "h": "bb4e3cd9be2e0078", "c": "import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport dotenv from \"dotenv\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconsole.log(\"🔥 [GENESIS_BOOT] entry file:\", __filename);\nconsole.log(\"🔥 [GENESIS_BOOT] node env:\", process.env.NODE_ENV);\nconsole.log(\"🔥 [GENESIS_BOOT] pid:\", process.pid);\nconsole.log(\"🧬 [GENESIS_BOOT] cwd:\", process.cwd());\nconsole.log(\"🧬 [GENESIS_BOOT] import.meta.url:\", import.meta.url);\ndotenv.config({ path: path.join(__dirname, \"..\", \".env\") });\ndotenv.config({ path: path.join(__dirname, \"..\", \".env.local\"), override: true });\n\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport { createServer } from \"node:http\";\nimport { createOrchestrator } from \"@castle/orchestrator\";\nimport { parseTextToCommand } from \"@castle/command-dsl\";\nimport { WS_MESSAGE, COMMAND, createEnvelope, safeJsonParse } from \"@castle/protocol\";\nimport { queryOpenData } from \"./openData.js\";\nimport { verifyClientToken } from \"./auth.js\";\nimport { runRhizohBrain } from \"./rhizohBrain.js\";\nimport { queryRhizohLlm } from \"./rhizohLlmGateway.js\";\nimport { rhizohGatewayTurn } from \"./rhizohGatewayTurn.js\";\nimport {\n RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n runRhizohVoiceTranscribeV3,\n rhizohVoiceTranscribeEnvV3\n} from \"./rhizohVoiceTranscribeV3.js\";\nimport { createHttpCorsPolicy, normalizeHttpOrigin, parseAllowedOriginsFromEnv } from \"./httpCorsPolicyV1.js\";\nimport {\n applyLanguagePropagationToLlmPayloadV1,\n applyLanguagePropagationToVoiceBodyV1,\n buildRhizohLanguagePropagationEchoV1,\n parseRhizohLanguagePropagationV1\n} from \"./rhizohLanguagePropagationV1.js\";\nimport { trimRhizohLlmGatewayPayloadV1 } from \"./rhizohLlmPayloadTrimV1.js\";\nimport {\n meshAppendDelta,\n meshContinuityAggregate,\n meshJoin,\n meshLeave,\n meshReplayFromNodeId,\n meshReplayFromSeq,\n meshSnapshot,\n meshSubscribeSse\n} from \"./presenceRoomMesh.js\";\nimport { runRhizohBrainV2 } from \"./rhizohBrainV2.js\";\nimport {\n listConnections,\n createConnection,\n updateConnection,\n deleteConnection,\n setDefaultConnection,\n resolveConnection\n} from \"./llmConnectionsStore.js\";\nimport { checkHttpRateLimit, getHttpClientIp } from \"./castleHttpRateLimit.js\";\nimport {\n submitAbuseReportV0,\n listModerationQueueV0,\n softBlockUserV0,\n removeSoftBlockV0,\n listSoftBlockedUsersV0\n} from \"./ops/moderationMvpV0.js\";\nimport { listRecentAgentSnapshotsV0 } from \"./ops/agentObservabilityV0.js\";\nimport { readAgentContainmentConfigV0 } from \"./ops/agentContainmentV0.js\";\nimport { readCostContainmentConfigV0, getCostContainmentStatsV0 } from \"./ops/costContainmentV0.js\";\nimport { getGlobalCostLedgerSnapshotV0, getGclBackendStatusV0 } from \"./ops/globalCostLedgerV0.js\";\nimport { evaluateEconomicStrategyL3V0 } from \"./ops/economicStrategyEngineL3V0.js\";\nimport { getPhasedRolloutStatsV0, readPhasedRolloutTierV0 } from \"./ops/phasedRolloutV0.js\";\nimport { buildUnifiedStateNarrativeV0 } from \"./ops/unifiedStateNarrativeV0.js\";\nimport { resolveHardeningTenantIdV0 } from \"./ops/narrativeTenantIsolationV0.js\";\nimport {\n classifyStressResponseV0,\n verifyStressTaxonomyCoverageV0,\n verifyStressConflictResolutionV0\n} from \"./ops/stressResponseTaxonomyV0.js\";\nimport { verifyResolutionStabilityEnvelopeV0 } from \"./ops/resolutionStabilityEnvelopeV0.js\";\nimport { logLlmAccess } from \"./castleLlmAudit.js\";\nimport { appendMemory, listMemories, getMemoryContext, getPersonaGoalMemory, setPersonaGoalMemory, autoCompactMemories } from \"./memoryStore.js\";\nimport { getFirebasePersistence } from \"./firebasePersistence.js\";\nimport { registerAgentIdentity, listAgentIdentities, getAgentIdentity, updateAgentIdentity } from \"./agentIdentityStore.js\";\nimport { queueAcademyEvent, listAcademyEvents, resolveAcademyEvent, runAcademyEventTick } from \"./academyEventEngine.js\";\nimport { enqueuePdfJob, getPdfJob } from \"./pdfPipeline.js\";\nimport {\n createPublishSession,\n listPublishSessions,\n updatePublishSession,\n addTranscript,\n listTranscripts,\n searchMetadata,\n registerDevice,\n listDevices,\n updateDevice,\n appendTelemetry,\n listTelemetry,\n createAutomation,\n listAutomations,\n tickAutomations,\n enqueueCommand,\n listCommands,\n updateCommandStatus,\n registerSocialChannel,\n listSocialChannels\n} from \"./studioOpsStore.js\";\nimport { metrics as infraMetrics } from \"./infra/metrics.js\";\nimport { renderRhizohPrometheusMetrics, rhizohEnterpriseMetrics } from \"./infra/rhizohEnterpriseMetrics.js\";\nimport { scoreHealth } from \"./infra/healthScore.js\";\nimport { getTraceById, listTracesBySession } from \"./infra/traceRegistry.js\";\nimport { canonicalEpistemicSealString, hashAndSignEpistemicSeal, EPISTEMIC_SEAL_SCHEMA } from \"./epistemicSeal.js\";\nimport { persistEpistemicLedgerBatch } from \"./epistemicLedgerStore.js\";\nimport { persistEpistemicForecastBatch } from \"./epistemicForecastStore.js\";\nimport { buildRhizohExternalGroundTruthPayload } from \"./rhizohExternalGroundTruthGateway.js\";\nimport { ingestRhizohExternalLossBatchHttp } from \"./rhizohExternalLossIngestGateway.js\";\nimport {\n ingestRhizohProductOutcomeHttp,\n listRhizohProductOutcomeAggregates,\n verifyRhizohOutcomeHmac,\n verifyRhizohOutcomeSourceToken\n} from \"./rhizohProductOutcomeIngestGateway.js\";\nimport { initRhizoh } from \"./rhizohProductionBootstrap.js\";\nimport { handleAcademicObservatoryExportGetV0 } from \"./rhizoh/academicObservatoryHttpV0.js\";\nimport {\n buildGenesisRuntimeSurfacePayload,\n recordGenesisEpistemicLedgerPersisted,\n recordGenesisEpistemicSealIssued,\n startGenesisCanonicalClock\n} from \"./genesisRuntimeSurfaceV0.js\";\nimport { registerGenesisSseClient, getGenesisContinuitySeq, setGenesisContinuityAfterPublishHook } from \"./genesisContinuityStreamHubV0.js\";\nimport { computeGenesisReplayFingerprintV0 } from \"./genesisReplayFingerprintV0.js\";\nimport { buildGenesisGatewayCapabilitiesV0 } from \"./genesisGatewayCapabilitiesV0.js\";\nimport {\n installGenesisCheckpointSurfaceGetter,\n noteGenesisCheckpointSeqCommitted,\n getLatestGenesisSignedCheckpoint\n} from \"./genesisContinuityCheckpointV0.js\";\nimport { hydrateGenesisContinuityPersistenceBootV0 } from \"./genesisContinuityHydrateBootV0.js\";\nimport { startGenesisContinuityInfraSampler } from \"./genesisContinuityInfraSamplerV0.js\";\nimport {\n genesisCheckpointQueryBySeqV0,\n genesisCheckpointQueryRangeV0,\n genesisCheckpointQueryLineageV0\n} from \"./genesisCheckpointQueryV0.js\";\nimport {\n genesisContinuityEventArchiveEnabled,\n queryGenesisContinuityEventArchiveV0,\n GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION\n} from \"./genesisContinuityEventArchiveV0.js\";\nimport { resolveGenesisReplayRouterV1, GENESIS_REPLAY_ROUTER_SCHEMA } from \"./genesisReplayRouterV1.js\";\nimport {\n computeGenesisReplayTemporalDiffV1,\n GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA\n} from \"./genesisReplayTemporalDiffV1.js\";\nimport { compareGenesisReplayEquivalenceV1 } from \"./genesisReplayEquivalenceV1.js\";\nimport { computeGenesisReplayAnalyticsV1 } from \"./genesisReplayAnalyticsV1.js\";\nimport { computeGenesisReplayEvolutionV1 } from \"./genesisReplayEvolutionV1.js\";\nimport { handleStripeCheckoutCreate, handleStripeMembershipWebhook } from \"./stripeWebhookMembership.js\";\nimport { buildRealityHealthPayload } from \"./edgeRealityConsistencyV1.js\";\nimport {\n assertWalPeerSocketAuthorityV0,\n buildGatewaySubstrateAuthoritySnapshotV0,\n logGatewaySubstrateAuthorityGuardsV0,\n validateWalPeerFeedSignatureV0\n} from \"./gatewaySubstrateAuthorityV0.js\";\nimport {\n buildSubstrateOperationalSnapshotV0,\n recordWalPeerRejectV0,\n renderSubstratePrometheusMetricsV0\n} from \"./infra/substrateOperationalMetrics.js\";\nimport { ingestClientSubstrateHealthV0 } from \"./rhizohSubstrateHealthIngestV0.js\";\nimport { recordSubstrateOtelEventV0 } from \"./infra/opentelemetrySubstrateV0.js\";\nimport {\n GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n getGenesisWorldObservationIngressStatusV0,\n ingestGenesisWorldObservationV0\n} from \"./genesisContinuityClientIngressV0.js\";\nimport {\n getGenesisContinuitySeqAuditV0,\n noteGenesisContinuitySeqAcceptedV0\n} from \"./genesisContinuitySeqGapV0.js\";\n\n/** Render / Fly / Railway: `PORT` — yoksa `CASTLE_GATEWAY_PORT` — yoksa 8090. */\nconst PORT =\n Number(process.env.PORT) ||\n Number(process.env.CASTLE_GATEWAY_PORT) ||\n 8090;\n/** Spiral / world WebSocket server — set after `wss` init; used by genesis runtime surface. */\nlet spiralWssForGenesis = null;\nconst MAX_MESSAGE_BYTES = Number(process.env.CASTLE_MAX_MESSAGE_BYTES || 32 * 1024);\nconst REQUIRED_GATEWAY_TOKEN = process.env.CASTLE_GATEWAY_TOKEN || \"\";\nconst ALLOWED_ORIGINS = parseAllowedOriginsFromEnv().fromList;\nconst httpCors = createHttpCorsPolicy();\n\n/** @returns {string|null} `Access-Control-Allow-Origin`; null = browser CORS fail. */\nfunction accessControlAllowOriginValue(req) {\n return httpCors.accessControlAllowOriginValue(req);\n}\n\nfunction applyHttpCorsHeaders(req, res) {\n return httpCors.applyHttpCorsHeaders(req, res);\n}\nconst REQUIRE_AUTH = process.env.CASTLE_REQUIRE_AUTH === \"true\";\nconst ALLOW_DEV_ANON = process.env.CASTLE_ALLOW_DEV_ANON !== \"false\";\nconst ALLOW_DEV_HTTP_UID = process.env.CASTLE_ALLOW_DEV_HTTP_UID !== \"false\";\n/** Presence mesh: allow `X-Castle-Guest-Id` when Firebase auth absent (default on). Set `false` to require auth only. */\nconst ALLOW_MESH_GUEST = process.env.CASTLE_ALLOW_MESH_GUEST !== \"false\";\nconst SOCIAL_RETRY_MAX = Math.max(0, Math.min(5, Number(process.env.CASTLE_SOCIAL_RETRY_MAX || 2)));\nconst SOCIAL_RETRY_BASE_MS = Math.max(100, Number(process.env.CASTLE_SOCIAL_RETRY_BASE_MS || 700));\nconst TELEGRAM_BOT_TOKEN = process.env.CASTLE_TELEGRAM_BOT_TOKEN || \"\";\nconst WHATSAPP_TOKEN = process.env.CASTLE_WHATSAPP_TOKEN || \"\";\nconst WHATSAPP_PHONE_NUMBER_ID = process.env.CASTLE_WHATSAPP_PHONE_NUMBER_ID || \"\";\nconst RL_RHIZOH_LLM_PER_MIN = Math.max(5, Number(process.env.CASTLE_RL_RHIZOH_LLM_PER_MIN || 40));\nconst RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN || 30)\n);\nconst RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN || 120));\nconst RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN = Math.max(8, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN || 48));\nconst RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN = Math.max(4, Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN || 24));\nconst RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN || 18)\n);\nconst RL_EPISTEMIC_SEAL_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_EPISTEMIC_SEAL_PER_MIN || 120));\nconst RL_EPISTEMIC_LOG_PER_MIN = Math.max(20, Number(process.env.CASTLE_RL_EPISTEMIC_LOG_PER_MIN || 240));\nconst EPISTEMIC_SEAL_SECRET = String(\n process.env.CASTLE_EPISTEMIC_SEAL_SECRET || process.env.CASTLE_GATEWAY_TOKEN || \"\"\n).trim();\nconst RL_LLM_CONN_TEST_PER_MIN = Math.max(2, Number(process.env.CASTLE_RL_LLM_CONNECTION_TEST_PER_MIN || 8));\n\n/** One-shot production warnings: CORS surface, auth toggle, dev bypass flags (observatory deploy hygiene). */\nfunction logProductionObservatorySurfaceGuardsV0() {\n if (String(process.env.NODE_ENV || \"\").toLowerCase() !== \"production\") return;\n const tag = \"[GATEWAY_PROD_GUARD]\";\n const primaryCors = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n const corsListEmpty = ALLOWED_ORIGINS.length === 0;\n if (corsListEmpty && !primaryCors) {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty and CASTLE_HTTP_CORS_ORIGIN empty — production still merges RHIZOH_BROWSER_ORIGINS_V1 defaults; set env for preview/staging hosts`\n );\n } else if (corsListEmpty && primaryCors && primaryCors !== \"*\") {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty — only CASTLE_HTTP_CORS_ORIGIN set; add web.app + firebaseapp.com (and previews) to CASTLE_ALLOWED_ORIGINS if clients use alternate hostnames`\n );\n }\n if (primaryCors && /localhost|127\\.0\\.0\\.1/i.test(primaryCors)) {\n console.warn(`${tag} CORS: CASTLE_HTTP_CORS_ORIGIN is localhost while NODE_ENV=production`);\n }\n if (REQUIRE_AUTH) {\n console.warn(\n `${tag} AUTH: CASTLE_REQUIRE_AUTH=true — clients must send a valid gateway credential (otherwise silent 401 on replay/SSE); EventSource cannot set custom headers unless proxied`\n );\n if (REQUIRED_GATEWAY_TOKEN.length < 16) {\n console.warn(`${tag} AUTH: CASTLE_GATEWAY_TOKEN missing or short while require-auth enabled`);\n }\n }\n if (ALLOW_DEV_ANON) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_ANON is not \"false\" — dev anonymous path may be live in production (set CASTLE_ALLOW_DEV_ANON=false)`\n );\n }\n if (ALLOW_DEV_HTTP_UID) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_HTTP_UID is not \"false\" — X-Castle-Dev-Uid bypass risk (set CASTLE_ALLOW_DEV_HTTP_UID=false)`\n );\n }\n}\n\nconst rhizohRuntime = initRhizoh();\n\n/** İstemci: llmKeySource veya keySource — env | user_connection | auto */\nfunction normalizeClientLlmKeySource(raw) {\n const s = String(raw ?? \"\").trim().toLowerCase();\n if (s === \"env\" || s === \"server\") return \"env\";\n if (s === \"user_connection\" || s === \"connection\" || s === \"user\") return \"user_connection\";\n if (s === \"auto\" || s === \"\") return \"auto\";\n return null;\n}\n\nfunction sendJson(res, statusCode, payload) {\n res.writeHead(statusCode, { \"Content-Type\": \"application/json; charset=utf-8\" });\n res.end(JSON.stringify(payload));\n}\n\nfunction logRhizohHealth(stage, detail = {}) {\n try {\n console.info(`[CASTLE_${String(stage || \"unknown\")}]`, detail && typeof detail === \"object\" ? detail : {});\n } catch {\n /* noop */\n }\n}\n\nfunction renderInfraMetricsProm() {\n const lines = [\n `castle_gateway_events_processed_total ${infraMetrics.eventsProcessed}`,\n `castle_gateway_duplicate_reject_total ${infraMetrics.duplicateRejects}`,\n `castle_gateway_enqueue_latency_ms ${infraMetrics.enqueueLatencyMs}`,\n `castle_gateway_queue_depth ${infraMetrics.queueDepth}`,\n `castle_gateway_queue_lag_seconds ${(Number(infraMetrics.queueLag || 0) / 1000).toFixed(3)}`,\n `castle_gateway_errors_total ${infraMetrics.errors}`\n ];\n const buckets = infraMetrics.enqueueLatencyBuckets || {};\n for (const k of Object.keys(buckets)) {\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"${k}\"} ${buckets[k]}`);\n }\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"+Inf\"} ${infraMetrics.eventsProcessed}`);\n if (process.env.CASTLE_SUBSTRATE_METRICS !== \"0\") {\n lines.push(renderSubstratePrometheusMetricsV0().trimEnd());\n }\n return lines.join(\"\\n\");\n}\n\nasync function fetchJsonWithTimeout(url, timeoutMs = 1200) {\n const ctl = new AbortController();\n const tid = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const res = await fetch(url, { signal: ctl.signal });\n if (!res.ok) return null;\n return await res.json();\n } catch {\n return null;\n } finally {\n clearTimeout(tid);\n }\n}\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction getHttpPathname(req) {\n let p = String(req.url || \"\").split(\"?\")[0];\n /* Reverse proxies / clients sometimes append `/` — avoid silent 404 on e.g. `/rhizoh/genesis/stream/`. */\n while (p.length > 1 && p.endsWith(\"/\")) {\n p = p.slice(0, -1);\n }\n return p;\n}\n\nfunction rhizohLlmEnvConfigured() {\n const p = String(process.env.CASTLE_LLM_PROVIDER || \"openai\").toLowerCase();\n const key =\n p === \"anthropic\"\n ? process.env.ANTHROPIC_API_KEY\n : p === \"gemini\"\n ? process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY\n : p === \"xai\"\n ? process.env.XAI_API_KEY\n : p === \"deepseek\"\n ? process.env.DEEPSEEK_API_KEY\n : p === \"mistral\"\n ? process.env.MISTRAL_API_KEY\n : p === \"openrouter\"\n ? process.env.OPENROUTER_API_KEY\n : process.env.OPENAI_API_KEY;\n return !!String(key || \"\").trim();\n}\n\n/**\n * @param {import(\"firebase-admin/firestore\").Firestore | null} db\n * @param {number} timeoutMs\n */\nasync function probeFirestoreReachable(db, timeoutMs = 2000) {\n if (!db) return { ok: false, reason: \"no_db\" };\n const timeout = sleep(timeoutMs).then(() => ({ ok: false, reason: \"timeout\" }));\n const work = db\n .collection(\"_castle_gateway_health\")\n .doc(\"ping\")\n .get()\n .then(() => ({ ok: true, reason: \"reachable\" }))\n .catch((e) => ({ ok: false, reason: String(e?.message || e || \"firestore_error\") }));\n return Promise.race([work, timeout]);\n}\n\nfunction stripHtml(text = \"\") {\n return String(text)\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \" \")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \" \")\n .replace(/<[^>]+>/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction haversineMeters(aLat, aLon, bLat, bLon) {\n const R = 6371000;\n const dLat = ((bLat - aLat) * Math.PI) / 180;\n const dLon = ((bLon - aLon) * Math.PI) / 180;\n const sa = Math.sin(dLat / 2);\n const sb = Math.sin(dLon / 2);\n const x = sa * sa + Math.cos((aLat * Math.PI) / 180) * Math.cos((bLat * Math.PI) / 180) * sb * sb;\n return 2 * R * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));\n}\n\nasync function checkPublishTargetHealth(protocol, target) {\n const p = String(protocol || \"\").toUpperCase();\n const t = String(target || \"\").trim();\n if (!t) return { ok: false, reason: \"target_missing\", shouldReconnect: false };\n if (p === \"RTMP\") {\n const ok = t.startsWith(\"rtmp://\") || t.startsWith(\"rtmps://\");\n return { ok, reason: ok ? \"rtmp_target_format_ok\" : \"invalid_rtmp_target\", shouldReconnect: !ok, reconnectInSec: 15 };\n }\n if (p === \"WHIP\" || p === \"SFU\") {\n try {\n const res = await fetch(t, { method: \"OPTIONS\" });\n const ok = res.status < 500;\n return { ok, reason: `http_${res.status}`, shouldReconnect: !ok, reconnectInSec: 8 };\n } catch {\n return { ok: false, reason: \"network_unreachable\", shouldReconnect: true, reconnectInSec: 8 };\n }\n }\n return { ok: true, reason: \"unchecked_protocol\", shouldReconnect: false };\n}\n\nasync function fetchTextFromUrl(url) {\n const res = await fetch(url, { method: \"GET\" });\n if (!res.ok) throw new Error(`fetch_${res.status}`);\n const ct = String(res.headers.get(\"content-type\") || \"\");\n if (ct.includes(\"application/pdf\")) throw new Error(\"pdf_binary_not_supported_directly\");\n const body = await res.text();\n return stripHtml(body).slice(0, 14000);\n}\n\nfunction buildTaskActionGraph(task = \"\", context = {}) {\n const t = String(task || \"\").toLowerCase();\n if (t.includes(\"dinner\") || t.includes(\"akşam yemeği\") || t.includes(\"aksam yemegi\")) {\n return {\n taskTitle: \"Dinner Prep Planner\",\n nodes: [\n { id: \"n1\", title: \"Kitchen preheat\", action: \"smart-oven.preheat\", params: { temperatureC: 190, mode: \"fan\" } },\n { id: \"n2\", title: \"Lighting comfort\", action: \"smart-light.set_scene\", params: { scene: \"warm_dinner\" } },\n { id: \"n3\", title: \"Fridge inventory check\", action: \"smart-fridge.inventory_scan\", params: {} },\n { id: \"n4\", title: \"Reminder notification\", action: \"smart-watch.notify\", params: { text: \"Dinner prep started.\" } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" },\n { from: \"n3\", to: \"n4\" }\n ]\n };\n }\n return {\n taskTitle: task || \"General Planner\",\n nodes: [\n { id: \"n1\", title: \"Safety check\", action: \"device.safety_check\", params: { context } },\n { id: \"n2\", title: \"Primary action\", action: \"device.execute_task\", params: { task } },\n { id: \"n3\", title: \"Report\", action: \"studio.timeline_note\", params: { task } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" }\n ]\n };\n}\n\nfunction buildEthicsPrograms() {\n return {\n policyVersion: \"v1\",\n tracks: [\n {\n id: \"elderly-care-assist\",\n audience: \"elderly\",\n title: \"Elderly Assistance Track\",\n principles: [\"human-in-the-loop\", \"consent-first\", \"low-friction alerts\"],\n modules: [\"medication-reminder\", \"fall-risk-check\", \"family-notify\", \"daily-companion-brief\"]\n },\n {\n id: \"kids-safe-learning\",\n audience: \"children\",\n title: \"Kids Safe Learning Track\",\n principles: [\"guardian-consent\", \"age-appropriate-language\", \"content-filtering\"],\n modules: [\"study-coach\", \"screen-time-balance\", \"safe-social-share\", \"emergency-contact\"]\n }\n ]\n };\n}\n\nasync function deliverViaTelegram(channel, text) {\n const chatId = String(channel.endpoint || \"\").trim();\n if (!TELEGRAM_BOT_TOKEN) throw new Error(\"telegram_token_missing\");\n if (!chatId) throw new Error(\"telegram_chat_id_missing\");\n const url = `https://api.telegram.org/bot${encodeURIComponent(TELEGRAM_BOT_TOKEN)}/sendMessage`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ chat_id: chatId, text })\n });\n if (!res.ok) throw new Error(`telegram_http_${res.status}`);\n return { provider: \"telegram\", status: res.status };\n}\n\nasync function deliverViaWhatsapp(channel, text) {\n const to = String(channel.endpoint || \"\").trim();\n if (!WHATSAPP_TOKEN) throw new Error(\"whatsapp_token_missing\");\n if (!WHATSAPP_PHONE_NUMBER_ID) throw new Error(\"whatsapp_phone_number_id_missing\");\n if (!to) throw new Error(\"whatsapp_to_missing\");\n const url = `https://graph.facebook.com/v22.0/${encodeURIComponent(WHATSAPP_PHONE_NUMBER_ID)}/messages`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${WHATSAPP_TOKEN}` },\n body: JSON.stringify({ messaging_product: \"whatsapp\", to, type: \"text\", text: { body: text } })\n });\n if (!res.ok) throw new Error(`whatsapp_http_${res.status}`);\n return { provider: \"whatsapp\", status: res.status };\n}\n\nasync function deliverViaWebhook(channel, text) {\n const endpoint = String(channel.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"webhook_endpoint_missing\");\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text, platform: channel.platform || \"webhook\", ts: Date.now() })\n });\n if (!res.ok) throw new Error(`webhook_http_${res.status}`);\n return { provider: \"webhook\", status: res.status };\n}\n\nasync function deliverSocialMessage(channel, text) {\n const platform = String(channel.platform || \"webhook\").toLowerCase();\n if (platform === \"telegram\") return deliverViaTelegram(channel, text);\n if (platform === \"whatsapp\") return deliverViaWhatsapp(channel, text);\n return deliverViaWebhook(channel, text);\n}\n\nasync function deliverSocialWithRetry(channel, text) {\n let attempt = 0;\n let lastError = null;\n while (attempt <= SOCIAL_RETRY_MAX) {\n try {\n const result = await deliverSocialMessage(channel, text);\n return { ok: true, attempt: attempt + 1, ...result };\n } catch (error) {\n lastError = error;\n if (attempt >= SOCIAL_RETRY_MAX) break;\n const jitter = Math.floor(Math.random() * 90);\n await sleep(SOCIAL_RETRY_BASE_MS * Math.pow(2, attempt) + jitter);\n }\n attempt += 1;\n }\n return { ok: false, attempt: attempt + 1, error: String(lastError?.message || \"delivery_failed\") };\n}\n\nasync function dispatchCommandToAdapter(device, command) {\n const adapter = String(command?.adapter || device?.adapter || \"websocket\").toLowerCase();\n const endpoint = String(device?.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"device_endpoint_missing\");\n if (adapter === \"mqtt\") {\n // MQTT delivery is proxied via a bridge endpoint (HTTP webhook style).\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n type: \"mqtt-command\",\n topic: `castle/devices/${device.id}/commands`,\n action: command.action,\n params: command.params || {}\n })\n });\n if (!res.ok) throw new Error(`mqtt_bridge_http_${res.status}`);\n return { ok: true, adapter, via: \"http-bridge\", status: res.status };\n }\n if (!endpoint.startsWith(\"ws://\") && !endpoint.startsWith(\"wss://\")) throw new Error(\"invalid_websocket_endpoint\");\n const sendResult = await new Promise((resolve, reject) => {\n const ws = new WebSocket(endpoint);\n const timer = setTimeout(() => {\n try {\n ws.close();\n } catch {\n /* noop */\n }\n reject(new Error(\"websocket_timeout\"));\n }, 3000);\n ws.on(\"open\", () => {\n ws.send(\n JSON.stringify({\n type: \"castle-command\",\n commandId: command.id,\n deviceId: command.deviceId,\n action: command.action,\n params: command.params || {}\n }),\n (err) => {\n clearTimeout(timer);\n ws.close();\n if (err) reject(new Error(\"websocket_send_failed\"));\n else resolve({ ok: true, adapter: \"websocket\", via: \"ws\" });\n }\n );\n });\n ws.on(\"error\", () => {\n clearTimeout(timer);\n reject(new Error(\"websocket_connect_failed\"));\n });\n });\n return sendResult;\n}\n\nfunction readHttpJson(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => {\n if (!buf) return resolve({});\n const payload = safeJsonParse(buf);\n if (!payload || typeof payload !== \"object\") return reject(new Error(\"invalid_json_payload\"));\n resolve(payload);\n });\n req.on(\"error\", reject);\n });\n}\n\n/** Ham gövde (HMAC doğrulaması için). */\nfunction readHttpBodyText(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => resolve(buf));\n req.on(\"error\", reject);\n });\n}\n\nfunction maskOpaqueId(value) {\n const t = String(value || \"\");\n if (!t) return \"(empty)\";\n if (t.length <= 8) return \"****\";\n return `${t.slice(0, 4)}…${t.slice(-2)}`;\n}\n\nfunction readGatewayHttpToken(req) {\n const x = String(req.headers?.[\"x-castle-gateway-token\"] || \"\").trim();\n if (x) return x;\n const auth = String(req.headers?.authorization || \"\");\n const m = /^Bearer\\s+(.+)$/i.exec(auth);\n return m ? String(m[1] || \"\").trim() : \"\";\n}\n\nasync function resolveHttpUser(req) {\n const authResult = await verifyClientToken(req);\n if (authResult?.ok && authResult.user?.uid) {\n return { ok: true, uid: String(authResult.user.uid), mode: authResult.kind || \"auth\" };\n }\n if (ALLOW_DEV_ANON && ALLOW_DEV_HTTP_UID) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) return { ok: true, uid: `dev-${devUid.slice(0, 64)}`, mode: \"dev-header\" };\n }\n return { ok: false, reason: authResult?.reason || \"auth_required\" };\n}\n\n/**\n * Epistemic ledger ingest — Firebase uid OR valid transport token + X-Castle-Dev-Uid.\n * Prod disables CASTLE_ALLOW_DEV_HTTP_UID; LLM may still work while telemetry 401'd without this.\n */\nasync function resolveEpistemicIngestActor(req) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok && tok === REQUIRED_GATEWAY_TOKEN) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) {\n return { ok: true, uid: `gw-telemetry-${devUid.slice(0, 64)}`, mode: \"gateway-token-dev-uid\" };\n }\n return { ok: false, reason: \"dev_uid_required\" };\n }\n if (tok) {\n return { ok: false, reason: \"gateway_token_mismatch\" };\n }\n }\n const auth = await resolveHttpUser(req);\n if (auth.ok) return auth;\n return { ok: false, reason: auth.reason || \"auth_required\" };\n}\n\n/** Mesh actor: Firebase uid, dev header, or `guest:<uuid>` via header / `guestId` query (SSE cannot set headers). */\nasync function resolveMeshActor(req) {\n const auth = await resolveHttpUser(req);\n if (auth.ok) return { ok: true, clientUid: auth.uid, mode: auth.mode };\n if (ALLOW_MESH_GUEST) {\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const gid = String(req.headers?.[\"x-castle-guest-id\"] || q.searchParams.get(\"guestId\") || \"\").trim();\n if (gid) {\n const normalized = gid.startsWith(\"guest:\") ? gid.slice(0, 180) : `guest:${gid.slice(0, 160)}`;\n return { ok: true, clientUid: normalized, mode: \"guest-header\" };\n }\n }\n return { ok: false, reason: \"mesh_auth_required\" };\n}\n\n/** Live genesis runtime JSON (GET /rhizoh/genesis/runtime + SSE infra sampler). */\nasync function buildGenesisRuntimeSurfacePayloadLive() {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n let spiralWsActive = 0;\n if (spiralWssForGenesis) {\n for (const c of spiralWssForGenesis.clients) {\n if (c.readyState === 1) spiralWsActive += 1;\n }\n }\n const base = buildGenesisRuntimeSurfacePayload({\n infraMetrics,\n scoredHealth: scored,\n rhizohEnterpriseMetrics,\n mesh: meshContinuityAggregate(),\n workerHealth: worker,\n port: PORT,\n spiralWebSocketClientsActive: spiralWsActive\n });\n return {\n ...base,\n replayFingerprint: computeGenesisReplayFingerprintV0(base),\n gatewayCapabilities: buildGenesisGatewayCapabilitiesV0(),\n genesisStream: {\n schema: \"castle.genesis.stream_cursor.v0\",\n lastAcceptedSeq: getGenesisContinuitySeq(),\n clientIngress: {\n schema: GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n contract: \"castle.world_observation.ingress_envelope.v1\",\n postPath: rhizohRuntime.routes.genesisIngress,\n closure: getGenesisWorldObservationIngressStatusV0()\n },\n seqContinuity: getGenesisContinuitySeqAuditV0(),\n eventArchive: genesisContinuityEventArchiveEnabled()\n ? {\n enabled: true,\n projection: GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION,\n queryPath: rhizohRuntime.routes.genesisContinuityEvents\n }\n : { enabled: false },\n replayRouter: {\n schema: GENESIS_REPLAY_ROUTER_SCHEMA,\n queryPath: rhizohRuntime.routes.genesisReplay,\n queryHint: \"from=&to= or range=from-to; optional type=&checkpoints=0\",\n temporalDiffPath: rhizohRuntime.routes.genesisReplayDiff,\n temporalDiffSchema: GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA,\n equivalencePath: rhizohRuntime.routes.genesisReplayEquivalence,\n analyticsPath: rhizohRuntime.routes.genesisReplayAnalytics,\n evolutionPath: rhizohRuntime.routes.genesisReplayEvolution\n }\n }\n };\n}\n\nconst httpServer = createServer(async (req, res) => {\n const corsAllow = applyHttpCorsHeaders(req, res);\n if (req.method === \"OPTIONS\") {\n if (!corsAllow && req.headers?.origin) {\n console.warn(\"[GATEWAY_CORS] preflight rejected for origin:\", req.headers.origin);\n }\n res.writeHead(corsAllow ? 204 : 403);\n res.end();\n return;\n }\n\n const pathname = getHttpPathname(req);\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/genesis/__ping\") {\n sendJson(res, 200, {\n ok: true,\n pid: process.pid,\n entry: __filename,\n time: Date.now(),\n genesis: true\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/epistemic/__probe\") {\n sendJson(res, 200, {\n ok: true,\n epistemicIngestActorV2: true,\n routes: {\n seal: rhizohRuntime.routes.epistemicSeal,\n logsBatch: rhizohRuntime.routes.epistemicLogsBatch\n },\n hasGatewayToken: Boolean(REQUIRED_GATEWAY_TOKEN),\n gatewayTokenLen: REQUIRED_GATEWAY_TOKEN ? REQUIRED_GATEWAY_TOKEN.length : 0\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/webhooks/stripe\") {\n await handleStripeMembershipWebhook(req, res);\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/billing/stripe/checkout\") {\n await handleStripeCheckoutCreate(req, res, { readHttpJson, resolveHttpUser, sendJson });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/live\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, live: false, dns: true, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n sendJson(res, 200, { ok: true, live: true, dns: true, service: \"castle-gateway\", ts: Date.now() });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/cors\") {\n const reqOrigin = normalizeHttpOrigin(req.headers?.origin);\n const allowOrigin = accessControlAllowOriginValue(req);\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n requestOrigin: reqOrigin || null,\n allowed: Boolean(allowOrigin),\n allowOrigin: allowOrigin || null,\n allowSetSize: httpCors.allowSet.size\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/ready\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, ready: false, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n try {\n const persistence = getFirebasePersistence();\n const llmProbe = rhizohLlmEnvConfigured();\n sendJson(res, 200, {\n ok: true,\n ready: true,\n dns: true,\n persistence: persistence.mode,\n service: \"castle-gateway\",\n gateway: \"ok\",\n llm: llmProbe ? \"ok\" : \"degraded\",\n spine: llmProbe ? \"ok\" : \"partial\"\n });\n } catch (e) {\n sendJson(res, 503, { ok: false, ready: false, reason: String(e?.message || e), service: \"castle-gateway\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/reality\") {\n const payload = buildRealityHealthPayload();\n sendJson(res, payload.ok ? 200 : 503, payload);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/substrate\") {\n const authority = buildGatewaySubstrateAuthoritySnapshotV0();\n const operational = buildSubstrateOperationalSnapshotV0();\n const drift = buildRealityHealthPayload();\n const ok = Boolean(authority.productionAuthoritySatisfied) && drift.ok;\n sendJson(res, ok ? 200 : 503, {\n ok,\n service: \"castle-gateway\",\n ts: Date.now(),\n authority,\n operational,\n drift: { ok: drift.ok, errors: drift.drift?.errors, warnings: drift.drift?.warnings }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/substrate/health\") {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const body = await readHttpJson(req);\n const uid = (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestClientSubstrateHealthV0(body, { uid, clientId: uid });\n if (!result.ok) {\n sendJson(res, 400, result);\n return;\n }\n recordSubstrateOtelEventV0(\"client_health_ingest\", { uid: String(uid).slice(0, 32) });\n sendJson(res, 200, result);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/deps\") {\n const t0 = Date.now();\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, {\n ok: false,\n dns: true,\n llm: false,\n firestore: false,\n phase: \"maintenance\",\n latencyMs: Date.now() - t0,\n service: \"castle-gateway\"\n });\n return;\n }\n const persistence = getFirebasePersistence();\n let firestoreOk = persistence.mode !== \"firebase\";\n let firestoreDetail = persistence.mode === \"file\" ? \"file_mode\" : \"not_applicable\";\n if (persistence.mode === \"firebase\" && persistence.db) {\n const pr = await probeFirestoreReachable(persistence.db, 2000);\n firestoreOk = pr.ok;\n firestoreDetail = pr.reason || (pr.ok ? \"reachable\" : \"fail\");\n } else if (persistence.mode === \"firebase\" && !persistence.db) {\n firestoreOk = false;\n firestoreDetail = \"firebase_not_initialized\";\n }\n const llmOk = rhizohLlmEnvConfigured();\n const overallOk = llmOk && firestoreOk;\n const latencyMs = Date.now() - t0;\n sendJson(res, 200, {\n ok: overallOk,\n dns: true,\n llm: llmOk,\n firestore: persistence.mode === \"firebase\" ? firestoreOk : false,\n persistence: persistence.mode,\n firestoreDetail,\n latencyMs,\n service: \"castle-gateway\",\n wsPort: PORT\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health\") {\n const persistence = getFirebasePersistence();\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n wsPort: PORT,\n persistence: persistence.mode,\n presenceMesh: \"sse\"\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/health\") {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n sendJson(res, 200, {\n ok: true,\n role: \"gateway\",\n queueLagMs: infraMetrics.queueLag,\n queueDepth: infraMetrics.queueDepth,\n errors: infraMetrics.errors,\n status: scored.status,\n score: scored.score,\n reasons: scored.reasons,\n worker: worker\n ? {\n status: worker.status,\n score: worker.score,\n reasons: worker.reasons\n }\n : null,\n timestamp: Date.now()\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisRuntime) {\n const payload = await buildGenesisRuntimeSurfacePayloadLive();\n sendJson(res, 200, payload);\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.genesisIngress) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const actor = await resolveMeshActor(req);\n const clientId = actor.ok ? actor.clientUid : (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestGenesisWorldObservationV0(body, { clientId });\n if (!result.ok) {\n const status =\n result.error === \"rate_limited\" || result.error === \"agent_spoke_throttled\"\n ? 429\n : result.error === \"payload_too_large\"\n ? 413\n : 400;\n sendJson(res, status, result);\n return;\n }\n sendJson(res, result.idempotent ? 200 : 202, result);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisStream) {\n const corsOrigin = accessControlAllowOriginValue(req);\n if (!corsOrigin && String(process.env.NODE_ENV || \"\").toLowerCase() === \"production\") {\n const og = String(req.headers?.origin || \"\").trim();\n if (og) {\n console.warn(\"[GATEWAY] genesis SSE: missing Access-Control-Allow-Origin for browser Origin (CORS whitelist mismatch):\", og);\n }\n }\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n res.write(\"retry: 3000\\n\\n\");\n res.write(\": genesis continuity stream\\n\\n\");\n res.write(\"event: genesis\\n\");\n res.write(`data: {\"booted\":true,\"pid\":${process.pid}}\\n\\n`);\n registerGenesisSseClient(res, req);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLatest) {\n const cp = getLatestGenesisSignedCheckpoint();\n if (!cp) {\n sendJson(res, 404, { ok: false, error: \"no_checkpoint_yet\", hint: \"wait_for_seq_interval_or_configure_signing_secret\" });\n return;\n }\n sendJson(res, 200, { ok: true, checkpoint: cp });\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(rhizohRuntime.routes.genesisCheckpointBySeqPrefix)) {\n const tail = pathname.slice(rhizohRuntime.routes.genesisCheckpointBySeqPrefix.length).split(\"/\")[0];\n const seq = parseInt(String(tail || \"\").trim(), 10);\n const out = await genesisCheckpointQueryBySeqV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"checkpoint_not_found\") {\n sendJson(res, 404, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointRange) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryRangeV0(from, to);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"range_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLineage) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const seq = parseInt(String(u.searchParams.get(\"seq\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryLineageV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"lineage_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisContinuityEvents) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const limit = parseInt(String(u.searchParams.get(\"limit\") || \"\").trim(), 10);\n const out = await queryGenesisContinuityEventArchiveV0(from, to, type, limit);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\" || out.error === \"event_archive_disabled\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"archive_file_too_large\") {\n sendJson(res, 413, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplay) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await resolveGenesisReplayRouterV1({\n from: fromSeq,\n to: toSeq,\n type,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayDiff) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const out = await computeGenesisReplayTemporalDiffV1({ from: fromSeq, to: toSeq, type });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEquivalence) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from1 = parseInt(String(u.searchParams.get(\"from1\") || \"\").trim(), 10);\n const to1 = parseInt(String(u.searchParams.get(\"to1\") || \"\").trim(), 10);\n const from2 = parseInt(String(u.searchParams.get(\"from2\") || \"\").trim(), 10);\n const to2 = parseInt(String(u.searchParams.get(\"to2\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await compareGenesisReplayEquivalenceV1({\n from1,\n to1,\n from2,\n to2,\n type,\n includeCheckpoints\n });\n if (\n !out.ok &&\n (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")\n ) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayAnalytics) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayAnalyticsV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEvolution) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const collapseWindows = parseInt(String(u.searchParams.get(\"collapseWindows\") || \"0\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayEvolutionV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n collapseWindows,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/metrics\") {\n res.writeHead(200, { \"Content-Type\": \"text/plain; version=0.0.4; charset=utf-8\" });\n res.end(renderInfraMetricsProm());\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.infraPrometheusMetrics) {\n if (process.env.CASTLE_PROMETHEUS_METRICS !== \"1\") {\n sendJson(res, 404, { ok: false, error: \"prometheus_metrics_disabled\" });\n return;\n }\n res.writeHead(200, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(renderRhizohPrometheusMetrics());\n return;\n }\n if (req.method === \"GET\" && pathname.startsWith(\"/infra/traces/\")) {\n const traceId = decodeURIComponent(pathname.slice(\"/infra/traces/\".length));\n const item = await getTraceById(traceId);\n if (!item) return sendJson(res, 404, { ok: false, error: \"trace_not_found\" });\n return sendJson(res, 200, { ok: true, trace: item });\n }\n if (req.method === \"GET\" && pathname === \"/infra/traces\") {\n const u = new URL(String(req.url || \"/infra/traces\"), `http://${req.headers.host || \"localhost\"}`);\n const sessionId = String(u.searchParams.get(\"sessionId\") || \"\").trim();\n const items = sessionId ? await listTracesBySession(sessionId) : [];\n return sendJson(res, 200, { ok: true, sessionId, traces: items });\n }\n\n /** Presence room mesh (C): JOIN / LEAVE / SNAPSHOT / DELTA / REPLAY (HTTP); SUBSCRIBE = SSE stream. */\n if (req.method === \"POST\" && pathname === \"/presence/mesh/join\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid, clientUid: actor.clientUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/leave\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshLeave(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/snapshot\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n sendJson(res, 200, meshSnapshot(roomUid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/delta\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, MAX_MESSAGE_BYTES);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const seq = meshAppendDelta(roomUid, actor.clientUid, {\n node: body?.node,\n projectionPatch: body?.projectionPatch,\n writerSubject: body?.writerSubject\n });\n sendJson(res, 200, { ok: true, roomUid, seq });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/replay\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const fromNodeId = body?.fromNodeId != null ? String(body.fromNodeId) : \"\";\n const fromSeq = body?.fromSeq;\n if (fromNodeId) {\n sendJson(res, 200, meshReplayFromNodeId(roomUid, fromNodeId));\n return;\n }\n sendJson(res, 200, meshReplayFromSeq(roomUid, fromSeq));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/presence/mesh/subscribe\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const roomUid = String(q.searchParams.get(\"roomUid\") || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n const write = (chunk) => {\n try {\n res.write(chunk);\n } catch {\n /* closed */\n }\n };\n const unsub = meshSubscribeSse(roomUid, write);\n const onClose = () => {\n unsub();\n meshLeave(roomUid, actor.clientUid);\n try {\n res.end();\n } catch {\n /* ignore */\n }\n };\n req.on(\"close\", onClose);\n req.on(\"aborted\", onClose);\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, uid: auth.uid, mode: auth.mode, items: await listConnections(auth.uid) });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const id = await createConnection(auth.uid, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"create_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n await updateConnection(auth.uid, id, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"update_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"DELETE\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n await deleteConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"delete_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections/test\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n if (!checkHttpRateLimit(`llm_conn_test:${auth.uid}`, RL_LLM_CONN_TEST_PER_MIN, 60_000)) {\n return sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n }\n const hasBodyKey = !!String(payload?.apiKey || \"\").trim();\n const result = await queryRhizohLlm(\n {\n message: String(payload?.message || \"Ping from Castle Gateway\"),\n context: payload?.context || { source: \"connection_test\" },\n provider: payload?.provider,\n model: payload?.model,\n apiKey: payload?.apiKey || \"\"\n },\n { allowExternalApiKey: true, llmKeySource: hasBodyKey ? \"auto\" : \"env\" }\n );\n logLlmAccess({\n route: \"/llm/connections/test\",\n uid: auth.uid,\n llmKeyBillingOwner: result.llmKeyBillingOwner,\n llmKeyOrigin: result.llmKeyOrigin,\n provider: result.provider,\n model: result.model,\n connectionId: null\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"connection_test_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAgentIdentities(auth.uid);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerAgentIdentity({\n ownerUid: auth.uid,\n agentId: payload?.agentId,\n role: payload?.role,\n personaSeed: payload?.personaSeed || {},\n capabilityLevel: payload?.capabilityLevel\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/agents/identities/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const owns = await getAgentIdentity(id);\n if (!owns || owns.ownerUid !== auth.uid) return sendJson(res, 403, { ok: false, error: \"forbidden\" });\n const payload = await readHttpJson(req);\n const row = await updateAgentIdentity(id, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/academy/events\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/academy/events\"}`);\n const status = String(url.searchParams.get(\"status\") || \"\");\n const items = await listAcademyEvents({ ownerUid: auth.uid, status, limit: 120 });\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/academy/events\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const ev = await queueAcademyEvent({\n type: payload?.type,\n roomId: payload?.roomId,\n ownerUid: auth.uid,\n participants: payload?.participants || [],\n topic: payload?.topic || \"academy\",\n payload: payload?.payload || {}\n });\n sendJson(res, 200, { ok: true, event: ev });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && String(req.url || \"\").startsWith(\"/academy/events/\") && String(req.url || \"\").endsWith(\"/resolve\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const parts = String(req.url || \"\").split(\"/\").filter(Boolean);\n const eventId = parts[2] || \"\";\n try {\n const payload = await readHttpJson(req);\n const out = await resolveAcademyEvent(eventId, payload || {});\n sendJson(res, 200, { ok: true, event: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_resolve_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/memory\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/memory\"}`);\n const scope = url.searchParams.get(\"scope\") === \"agents\" ? \"agents\" : \"users\";\n const id = String(url.searchParams.get(\"id\") || (scope === \"agents\" ? \"\" : auth.uid));\n const kind = String(url.searchParams.get(\"kind\") || \"\");\n const limit = Number(url.searchParams.get(\"limit\") || 40);\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n sendJson(res, 200, { ok: true, scope, id, kind, items: await listMemories({ scope, id, kind, limit }) });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const profile = await getPersonaGoalMemory(auth.uid);\n sendJson(res, 200, { ok: true, profile });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const profile = await setPersonaGoalMemory({\n uid: auth.uid,\n personaPatch: payload?.personaPatch || {},\n goals: Array.isArray(payload?.goals) ? payload.goals : null,\n preferencesPatch: payload?.preferencesPatch || {}\n });\n sendJson(res, 200, { ok: true, profile });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"profile_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/compact\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const out = await autoCompactMemories({ scope, id });\n sendJson(res, 200, { ok: true, scope, id, ...out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_compact_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/context\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const context = await getMemoryContext({\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n query: String(payload?.query || \"\"),\n limit: Number(payload?.limit || 80)\n });\n sendJson(res, 200, { ok: true, context });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_context_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/place-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const placeName = String(payload?.placeName || \"\").trim();\n if (!placeName) return sendJson(res, 400, { ok: false, error: \"placeName_required\" });\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const persona = String(payload?.waypointPersona || payload?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(placeName)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Place: ${placeName}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Give a concise conversational briefing about this place for a walking-route companion.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-place-brief\", placeName, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `PLACE_BRIEF:${placeName} => ${String(summary.reply || \"\").slice(0, 900)}`,\n tags: [\"place\", \"brief\", \"event-layer\"],\n importance: 0.62,\n meta: { placeName }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"place-brief\",\n text: `${placeName} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: {\n placeName,\n directive: summary.directive,\n lat: hit?.lat ? Number(hit.lat) : null,\n lon: hit?.lon ? Number(hit.lon) : null\n }\n });\n sendJson(res, 200, { ok: true, place: hit || null, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"place_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/route-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const waypoints = Array.isArray(payload?.waypoints) ? payload.waypoints.slice(0, 24) : [];\n const out = [];\n for (const wp of waypoints) {\n const name = String(wp?.name || \"\").trim();\n if (!name) continue;\n const persona = String(wp?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(name)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Waypoint: ${name}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Generate a concise spoken briefing for route waypoint.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-route-brief\", waypoint: name, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n out.push({\n name,\n persona,\n place: hit || null,\n reply: summary.reply,\n directive: summary.directive\n });\n }\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `ROUTE_BRIEF: waypoints=${out.length} ${out.map((x) => `${x.name}:${x.persona}`).join(\" | \")}`.slice(0, 1300),\n tags: [\"route\", \"waypoint\", \"brief\", \"event-layer\"],\n importance: 0.74,\n meta: { count: out.length }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"route-brief\",\n text: out.map((x) => `${x.name}:${String(x.reply || \"\").slice(0, 140)}`).join(\" | \").slice(0, 1400),\n roomId: \"studio-main\",\n meta: {\n waypointCount: out.length,\n waypoints: out.map((x) => ({\n name: x.name,\n persona: x.persona,\n lat: x.place?.lat ? Number(x.place.lat) : null,\n lon: x.place?.lon ? Number(x.place.lon) : null\n }))\n }\n });\n sendJson(res, 200, { ok: true, items: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"route_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/pdf-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const title = String(payload?.title || \"document\");\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const providedText = String(payload?.text || \"\");\n const url = String(payload?.url || \"\");\n let content = providedText;\n if (!content && url) content = await fetchTextFromUrl(url);\n if (!content) return sendJson(res, 400, { ok: false, error: \"text_or_supported_url_required\" });\n const summary = await queryRhizohLlm(\n {\n message: `Summarize this document and provide key points + practical actions.\\nTitle:${title}\\nContent:\\n${content.slice(0, 12000)}`,\n context: { source: \"event-layer-pdf-brief\", title },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `DOC_BRIEF:${title} => ${String(summary.reply || \"\").slice(0, 1000)}`,\n tags: [\"document\", \"brief\", \"library\", \"event-layer\"],\n importance: 0.7,\n meta: { title, url: url || null }\n });\n await addTranscript(auth.uid, {\n source: \"library-brief\",\n eventType: \"pdf-brief\",\n text: `${title} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { title }\n });\n sendJson(res, 200, { ok: true, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/pdf/upload\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req, 8 * 1024 * 1024);\n const job = await enqueuePdfJob({\n uid: auth.uid,\n fileName: payload?.fileName || \"document.pdf\",\n bytesBase64: payload?.contentBase64\n });\n sendJson(res, 200, { ok: true, job });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_upload_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/session\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createPublishSession(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session\",\n text: `Publish session armed: ${row.protocol} ${row.target}`.slice(0, 500),\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: row.id, bridge: row.bridge }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/studio/publish/sessions\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listPublishSessions(auth.uid, 80);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/health-check\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const protocol = String(payload?.protocol || \"WHIP\");\n const target = String(payload?.target || \"\");\n const health = await checkPublishTargetHealth(protocol, target);\n if (payload?.sessionId) {\n await updatePublishSession(auth.uid, String(payload.sessionId), {\n status: health.ok ? \"healthy\" : \"degraded\",\n reconnectPolicy: {\n enabled: true,\n backoffSec: [2, 5, 10, 20],\n nextRetryInSec: health.reconnectInSec || 0,\n maxRetries: 8,\n reason: health.reason\n }\n });\n }\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-health\",\n text: `${protocol} ${target} => ${health.ok ? \"healthy\" : \"degraded\"} (${health.reason})`,\n roomId: \"studio-main\",\n meta: { protocol, target, reconnect: !!health.shouldReconnect }\n });\n sendJson(res, 200, { ok: true, health });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_health_check_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/studio/publish/sessions/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updatePublishSession(auth.uid, id, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session-update\",\n text: `Session ${id} -> ${row.status || \"updated\"}`,\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: id, status: row.status, ingestUrl: row.ingestUrl || \"\" }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/transcripts\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/transcripts\"}`);\n const limit = Math.max(1, Math.min(200, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await listTranscripts(auth.uid, limit);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/transcripts\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await addTranscript(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"transcript_write_failed\" });\n }\n return;\n }\n\n /** E1: broadcast intent → GreenRoom capability → transcript + memory artifact */\n if (req.method === \"POST\" && req.url === \"/studio/capabilities/greenroom\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const traceId = String(payload?.traceId || `TRC-${Date.now().toString(36).toUpperCase()}`);\n const title = String(payload?.title || \"Castle Live Broadcast\").slice(0, 200);\n const intentRaw = String(payload?.intentRaw || \"\").slice(0, 800);\n const audienceEstimate = Math.max(0, Math.min(50_000_000, Number(payload?.audienceEstimate) || 1200));\n const roomId = String(payload?.roomId || \"greenroom-main\");\n const transcriptRow = await addTranscript(auth.uid, {\n source: \"rhizoh\",\n eventType: \"BROADCAST_ROUTED\",\n text: `GreenRoom · ${title}${intentRaw ? `\\nIntent: ${intentRaw}` : \"\"}`,\n roomId,\n meta: {\n ack: \"BROADCAST_ROUTED\",\n capability: \"greenroom\",\n intentType: \"broadcast\",\n traceId,\n audienceEstimate,\n artifact: {\n kind: \"broadcast_card\",\n title,\n traceId,\n roomId,\n createdAt: Date.now()\n }\n }\n });\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n text: `GREENROOM_BROADCAST:${title} trace=${traceId}`,\n tags: [\"broadcast\", \"greenroom\", \"artifact\", \"castle-library\"],\n importance: 0.72,\n kind: \"episodic\",\n meta: { traceId, capability: \"greenroom\", transcriptId: transcriptRow.id }\n });\n sendJson(res, 200, {\n ok: true,\n ack: \"BROADCAST_ROUTED\",\n traceId,\n transcript: transcriptRow,\n replayPath: `/replay/${encodeURIComponent(traceId)}`,\n sharePath: `/replay/${encodeURIComponent(traceId)}`\n });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"greenroom_capability_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/metadata/search\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/metadata/search\"}`);\n const q = String(url.searchParams.get(\"q\") || \"\");\n const limit = Math.max(1, Math.min(300, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await searchMetadata(auth.uid, q, limit);\n sendJson(res, 200, { ok: true, q, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerDevice(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, device: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"device_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const lat = Number(payload?.lat);\n const lon = Number(payload?.lon);\n const speed = Math.max(0, Number(payload?.speed || 0));\n const cfgCenterLat = Number(process.env.CASTLE_GEOFENCE_CENTER_LAT || 41.0082);\n const cfgCenterLon = Number(process.env.CASTLE_GEOFENCE_CENTER_LON || 28.9784);\n const cfgRadiusM = Math.max(100, Number(process.env.CASTLE_GEOFENCE_RADIUS_M || 55000));\n const cfgSpeedCap = Math.max(1, Number(process.env.CASTLE_SPEED_CAP_MPS || 35));\n const dist = Number.isFinite(lat) && Number.isFinite(lon) ? haversineMeters(cfgCenterLat, cfgCenterLon, lat, lon) : 0;\n const geofenceOk = !Number.isFinite(dist) ? true : dist <= cfgRadiusM;\n const speedOk = speed <= cfgSpeedCap;\n const failsafe = !(geofenceOk && speedOk);\n const safety = {\n geofenceOk,\n speedOk,\n failsafe,\n distanceFromCenterM: Number.isFinite(dist) ? dist : null,\n speedCapMps: cfgSpeedCap\n };\n const row = await appendTelemetry(auth.uid, deviceId, {\n lat,\n lon,\n speed,\n battery: payload?.battery,\n mode: payload?.mode || (failsafe ? \"FAILSAFE_HOLD\" : \"ACTIVE\"),\n raw: payload?.raw || {}\n });\n await updateDevice(auth.uid, deviceId, {\n status: failsafe ? \"failsafe\" : \"active\",\n lastTelemetryAt: row.ts,\n safetyState: safety\n });\n if (failsafe) {\n await addTranscript(auth.uid, {\n source: \"robotics-guardrails\",\n eventType: \"failsafe\",\n text: `Device ${deviceId} failsafe triggered geofenceOk=${geofenceOk} speedOk=${speedOk}`,\n roomId: \"studio-main\",\n meta: { deviceId, safety }\n });\n }\n sendJson(res, 200, { ok: true, telemetry: row, safety, command: failsafe ? \"FAILSAFE_HOLD\" : \"CONTINUE\" });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"telemetry_ingest_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listTelemetry(auth.uid, 150);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createAutomation(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"automation-scheduled\",\n text: `Automation scheduled: ${row.title} at ${new Date(row.scheduleAt).toLocaleString()}`,\n roomId: \"studio-main\",\n meta: { automationId: row.id, action: row.action, deviceId: row.deviceId }\n });\n sendJson(res, 200, { ok: true, automation: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"automation_create_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAutomations(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listDevices(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const devs = await listDevices(auth.uid, 500);\n const d = devs.find((x) => x.id === deviceId);\n if (!d) return sendJson(res, 404, { ok: false, error: \"device_not_found\" });\n const action = String(payload?.action || \"\");\n const caps = Array.isArray(d.capabilityProfile) ? d.capabilityProfile : [];\n if (action && caps.length && !caps.includes(action) && !caps.includes(\"*\")) {\n return sendJson(res, 400, { ok: false, error: \"unsupported_action_for_device\", capabilityProfile: caps });\n }\n const cmd = await enqueueCommand(auth.uid, {\n deviceId,\n action: action || \"NOOP\",\n params: payload?.params || {},\n adapter: payload?.adapter || d.adapter || \"websocket\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-command-queue\",\n eventType: \"command-queued\",\n text: `${cmd.deviceId} -> ${cmd.action}`,\n roomId: \"studio-main\",\n meta: { commandId: cmd.id, adapter: cmd.adapter }\n });\n sendJson(res, 200, { ok: true, command: cmd });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listCommands(auth.uid, 200);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/robotics/commands/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updateCommandStatus(auth.uid, id, payload?.status || \"sent\");\n sendJson(res, 200, { ok: true, command: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/rhizoh/bridge\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const result = await runRhizohBrainV2({\n message: String(payload?.message || \"robotics bridge command\"),\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n worldState: payload?.worldState || {},\n deviceState: payload?.deviceState || {},\n intent: \"robotics-mechanics\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-bridge\",\n eventType: \"robotics-command\",\n text: `Bridge -> ${String(result.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { directive: result.directive, agentId: payload?.agentId || \"\" }\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"robotics_bridge_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/planner/graph\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const graph = buildTaskActionGraph(String(payload?.task || \"\"), payload?.context || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"planner-graph\",\n text: `Planner graph built: ${graph.taskTitle} nodes=${graph.nodes.length}`,\n roomId: \"studio-main\",\n meta: { taskTitle: graph.taskTitle, nodeCount: graph.nodes.length }\n });\n sendJson(res, 200, { ok: true, graph });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"planner_graph_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerSocialChannel(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, channel: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_channel_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listSocialChannels(auth.uid, 100);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/broadcast\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const text = String(payload?.text || \"\").slice(0, 1200);\n const channels = await listSocialChannels(auth.uid, 100);\n const enabled = channels.filter((c) => c.status === \"enabled\");\n const deliveries = [];\n for (const c of enabled) {\n const out = await deliverSocialWithRetry(c, text);\n deliveries.push({ id: c.id, platform: c.platform, ...out });\n }\n const okCount = deliveries.filter((d) => d.ok).length;\n await addTranscript(auth.uid, {\n source: \"social-layer\",\n eventType: \"social-broadcast\",\n text: `Broadcast delivered ${okCount}/${enabled.length} channel(s): ${text.slice(0, 240)}`,\n roomId: \"studio-main\",\n meta: { deliveries }\n });\n sendJson(res, 200, { ok: true, sentChannels: okCount, totalChannels: enabled.length, deliveries });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_broadcast_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/ethics/programs\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, ...buildEthicsPrograms() });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands/dispatch\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const all = await listCommands(auth.uid, 200);\n const queued = all.filter((c) => String(c.status || \"queued\") === \"queued\").slice(0, 50);\n const devices = await listDevices(auth.uid, 500);\n const byId = new Map(devices.map((d) => [d.id, d]));\n const results = [];\n for (const cmd of queued) {\n const dev = byId.get(cmd.deviceId);\n if (!dev) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: \"device_not_found\" });\n continue;\n }\n try {\n const dispatched = await dispatchCommandToAdapter(dev, cmd);\n await updateCommandStatus(auth.uid, cmd.id, \"sent\");\n results.push({ id: cmd.id, ok: true, adapter: dispatched.adapter, via: dispatched.via });\n } catch (error) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: String(error?.message || \"dispatch_failed\") });\n }\n }\n await addTranscript(auth.uid, {\n source: \"robotics-command-dispatch\",\n eventType: \"command-dispatch-batch\",\n text: `Dispatch batch size=${results.length} ok=${results.filter((x) => x.ok).length}`,\n roomId: \"studio-main\",\n meta: { results: results.slice(0, 30) }\n });\n sendJson(res, 200, { ok: true, processed: results.length, results });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"dispatch_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/pdf/jobs/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n const job = await getPdfJob(auth.uid, id);\n if (!job) return sendJson(res, 404, { ok: false, error: \"pdf_job_not_found\" });\n sendJson(res, 200, { ok: true, job });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const row = await appendMemory({\n scope,\n id,\n text: payload?.text,\n tags: payload?.tags || [],\n importance: payload?.importance,\n kind: payload?.kind || \"episodic\",\n meta: { uid: auth.uid, ...(payload?.meta || {}) }\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.externalTruth) {\n try {\n if (process.env.RHIZOH_EXTERNAL_TRUTH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_truth:${ip}`, RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n sendJson(res, 200, buildRhizohExternalGroundTruthPayload(req));\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.productOutcome) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const rawBody = await readHttpBodyText(req, 64 * 1024);\n const body = rawBody.trim() ? safeJsonParse(rawBody) : null;\n if (!body || typeof body !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"invalid_json_payload\" });\n return;\n }\n const sigHdr = String(req.headers?.[\"x-rhizoh-outcome-signature\"] || \"\").trim();\n const hmacV = verifyRhizohOutcomeHmac(rawBody, sigHdr);\n if (!hmacV.ok) {\n sendJson(res, 401, { ok: false, error: hmacV.error || \"outcome_hmac_invalid\" });\n return;\n }\n const srcTokHdr = String(req.headers?.[\"x-rhizoh-outcome-source-token\"] || \"\").trim();\n const srcV = verifyRhizohOutcomeSourceToken(body.source, srcTokHdr);\n if (!srcV.ok) {\n sendJson(res, 401, { ok: false, error: srcV.error || \"outcome_source_token_invalid\" });\n return;\n }\n const subjectKey = String(body.subjectKey || \"\").trim().slice(0, 128);\n if (\n subjectKey &&\n !checkHttpRateLimit(\n `rhizoh_product_outcome_subj:${subjectKey}`,\n RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN,\n 60_000\n )\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_subject\" });\n return;\n }\n const result = ingestRhizohProductOutcomeHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n schemaVersion: \"1.1.0\",\n aggregate: result.aggregate,\n integrity: result.integrity\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"outcome_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.productOutcomeAggregate) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome_agg:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const u = new URL(String(req.url || \"/\"), \"http://localhost\");\n const cohortId = u.searchParams.get(\"cohortId\") || \"\";\n const decisionFingerprint = u.searchParams.get(\"decisionFingerprint\") || \"\";\n const limit = u.searchParams.get(\"limit\") || \"50\";\n const rows = listRhizohProductOutcomeAggregates({\n cohortId: cohortId || undefined,\n decisionFingerprint: decisionFingerprint || undefined,\n limit: Number(limit) || 50\n });\n sendJson(res, 200, { ok: true, schemaVersion: \"1.1.0\", rows });\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.externalLossBatch) {\n try {\n if (process.env.RHIZOH_EXTERNAL_LOSS_BATCH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_loss_batch:${ip}`, RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const result = ingestRhizohExternalLossBatchHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, { ok: true, accepted: result.accepted ?? 0 });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"batch_failed\") });\n }\n return;\n }\n\n function moderationAdminOk(req) {\n const expected = String(process.env.CASTLE_MODERATION_ADMIN_KEY || \"\").trim();\n if (!expected) return false;\n const got = String(req.headers[\"x-castle-moderation-key\"] || \"\").trim();\n return got.length > 0 && got === expected;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.opsAbuseReport) {\n try {\n const body = await readHttpJson(req, 32 * 1024);\n const auth = await resolveHttpUser(req);\n const entry = submitAbuseReportV0({\n reporterUid: auth.ok ? auth.uid : null,\n targetUid: body?.targetUid ?? null,\n category: body?.category,\n detail: body?.detail,\n traceId: body?.traceId ?? null\n });\n sendJson(res, 202, { ok: true, reportId: entry.id, status: entry.status });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsModerationQueue) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n queue: listModerationQueueV0(100),\n softBlocked: listSoftBlockedUsersV0()\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-block\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const out = softBlockUserV0(body?.uid, body?.reason);\n sendJson(res, out.ok ? 200 : 400, out);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-unblock\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n sendJson(res, 200, removeSoftBlockV0(body?.uid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsAgentSnapshots) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n const limit = Number(new URL(req.url || \"\", \"http://local\").searchParams.get(\"limit\") || 50);\n sendJson(res, 200, { ok: true, snapshots: listRecentAgentSnapshotsV0(limit) });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.academicObservatoryExport) {\n const auth = await resolveHttpUser(req);\n const out = handleAcademicObservatoryExportGetV0(req, auth);\n sendJson(res, out.status, out.body);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsHardeningStatus) {\n const auth = await resolveHttpUser(req);\n const principal = auth.ok ? `uid:${auth.uid}` : null;\n const gclSnap = await getGlobalCostLedgerSnapshotV0(principal);\n const economicStrategy = evaluateEconomicStrategyL3V0(gclSnap);\n const hardeningTenantId = resolveHardeningTenantIdV0(auth, req);\n const unifiedState = await buildUnifiedStateNarrativeV0({\n tenantId: hardeningTenantId ?? undefined,\n platformScope: !hardeningTenantId,\n principal\n });\n sendJson(res, 200, {\n ok: true,\n schema: \"rhizoh.ops.hardening_status.v0\",\n containment: readAgentContainmentConfigV0(),\n cost: readCostContainmentConfigV0(),\n gcl: {\n backend: await getGclBackendStatusV0(),\n snapshot: gclSnap\n },\n economicStrategyL3: economicStrategy,\n phasedRollout: await getPhasedRolloutStatsV0(),\n tier: readPhasedRolloutTierV0(),\n principalCost: auth.ok ? await getCostContainmentStatsV0(principal) : null,\n stressTaxonomy: verifyStressTaxonomyCoverageV0(),\n stressConflictResolution: verifyStressConflictResolutionV0(),\n resolutionStabilityEnvelope: verifyResolutionStabilityEnvelopeV0(),\n unifiedState: {\n tenantScope: {\n tenantId: unifiedState.tenantScope?.tenantId,\n scope: unifiedState.tenantScope?.scope,\n noGlobalDerivedState: unifiedState.tenantScope?.noGlobalDerivedState\n },\n narrativeFingerprint: unifiedState.narrativeFingerprint,\n screenshotScopeWatermark: unifiedState.screenshotScopeWatermark,\n interpretationSafetyContract: unifiedState.interpretationSafetyContract,\n interpretationUxContract: {\n schema: unifiedState.interpretationUxContract?.schema,\n layerOrder: unifiedState.interpretationUxContract?.layerOrder,\n narrativeNeverVisuallyDominant: unifiedState.interpretationUxContract?.narrativeNeverVisuallyDominant\n },\n governance: {\n narrativeIsNotDecisionLayer: unifiedState.governance.narrativeIsNotDecisionLayer,\n displayOrder: unifiedState.governance.displayOrder,\n trustPosture: unifiedState.governance.trustPosture,\n interpretationUxContract: unifiedState.governance.interpretationUxContract?.schema\n },\n humanOps: {\n decisionTier: unifiedState.humanOps?.humanDecisionScaling?.tier,\n decisionOwner: unifiedState.humanOps?.humanDecisionScaling?.decisionOwner,\n opsRouteId: unifiedState.humanOps?.humanDecisionOpsRunbook?.routing?.routeId,\n slaAckMinutes: unifiedState.humanOps?.humanDecisionOpsRunbook?.sla?.ackMinutes,\n uiDisclaimerRequired: unifiedState.humanOps?.narrativeUiSafety?.displayRules?.requireDisclaimerBanner,\n misreadHighResidual: unifiedState.humanOps?.misreadSimulation?.highResidualCount,\n propagationHighResidual: unifiedState.humanOps?.socialPropagationSimulation?.highResidualCount,\n worstWatermarkSurvivability:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.worstWatermarkSurvivability,\n dominantDistortionSource:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.dominantDistortionSource\n },\n culturalRisk: {\n perceptualRiskClass: unifiedState.culturalRisk?.perceptualRiskClass,\n trustFork: unifiedState.culturalRisk?.trustDynamics?.fork,\n trustDecayScore: unifiedState.culturalRisk?.trustDynamics?.scores?.trustDecay,\n mythologyScore: unifiedState.culturalRisk?.trustDynamics?.scores?.mythology,\n confidenceDetachmentExposure:\n unifiedState.culturalRisk?.trustDynamics?.axes?.confidenceDetachmentExposure\n },\n appliedSystemsLayer: {\n schema: unifiedState.appliedSystemsLayer?.schema,\n researchOutputType: unifiedState.appliedSystemsLayer?.researchGovernance?.outputType,\n roboticsBlockActuation: unifiedState.appliedSystemsLayer?.roboticsGrounding?.blockActuation,\n spiralRumorCount:\n unifiedState.appliedSystemsLayer?.spiralMMOGameKernel?.gameLayer?.rumors?.length\n },\n actionSemanticGovernance: {\n schema: unifiedState.actionSemanticGovernance?.schema,\n highRiskActionCount: unifiedState.actionSemanticGovernance?.highRiskActionCount,\n loopClosed: unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.loopClosed,\n authorityCollapseMitigated:\n unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.risks?.authorityCollapse\n ?.mitigated\n },\n actionContextResolution: {\n schema: unifiedState.actionContextResolution?.schema,\n contextFingerprint: unifiedState.actionContextResolution?.contextFingerprint?.fingerprintHash,\n domainCollision: unifiedState.actionContextResolution?.domainCollision?.detected,\n deployAgentSpiralBlocked:\n unifiedState.actionContextResolution?.actionOverloadingRisk?.deployAgentExample\n ?.spiral_mmo === \"blocked\",\n roboticsActuationEligible:\n unifiedState.actionContextResolution?.roboticsActuationCorrectness?.actuationEligible\n },\n epistemicCoherence: {\n schema: unifiedState.epistemicCoherence?.schema,\n verdict: unifiedState.epistemicCoherence?.systemCoherence?.verdict,\n coherenceScore: unifiedState.epistemicCoherence?.systemCoherence?.score,\n contradictionCount: unifiedState.epistemicCoherence?.crossLayerContradictions?.count,\n operatorDecision: unifiedState.epistemicCoherence?.uxCompression?.operatorDecision,\n headlineTr: unifiedState.epistemicCoherence?.uxCompression?.headline?.tr,\n falseSafetyWarning: Boolean(\n unifiedState.epistemicCoherence?.uxCompression?.falseSafetyWarning\n )\n },\n coherenceAuthorityBoundary: {\n schema: unifiedState.coherenceAuthorityBoundary?.schema,\n bindingValid: unifiedState.coherenceAuthorityBoundary?.eclAuthorityBinding?.valid,\n canExecute: unifiedState.coherenceAuthorityBoundary?.contract?.can_execute,\n mandatoryBannerTr:\n unifiedState.coherenceAuthorityBoundary?.disclaimers?.mandatoryBanner?.tr\n },\n decisionLatencyGovernance: {\n schema: unifiedState.decisionLatencyGovernance?.schema,\n latencyTier: unifiedState.decisionLatencyGovernance?.latencyTier,\n inflationScore: unifiedState.decisionLatencyGovernance?.latencyInflation?.score,\n fastPathEligible: unifiedState.decisionLatencyGovernance?.fastPath?.eligible,\n primaryActionId: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.primaryActionId,\n ackSlaMinutes: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.ackSlaMinutes,\n certaintyCap: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.certaintyCap,\n uncertaintyItems:\n unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.uncertaintyEnvelope?.itemCount\n },\n decisionPacketUncertaintyBoundary: {\n schema: unifiedState.decisionPacketUncertaintyBoundary?.schema,\n invariantValid: unifiedState.decisionPacketUncertaintyBoundary?.invariantCheck?.valid,\n centralInvariant: unifiedState.decisionPacketUncertaintyBoundary?.centralInvariant\n },\n epistemicDecisionPacing: {\n schema: unifiedState.epistemicDecisionPacing?.schema,\n queueSaturationTier:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation?.tier,\n operatorProcessingLatency:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation\n ?.operatorProcessingLatency,\n postureWindowCount:\n unifiedState.epistemicDecisionPacing?.temporaryStaticPostureWindows\n ?.temporaryStaticPostureWindows?.windowCount,\n executionBoundariesValid:\n unifiedState.epistemicDecisionPacing?.executionBoundaries?.valid,\n grantsExecutionApproval: unifiedState.epistemicDecisionPacing?.grantsExecutionApproval\n },\n epistemicTemporalCoherence: {\n schema: unifiedState.epistemicTemporalCoherence?.schema,\n allInvariantsValid: unifiedState.epistemicTemporalCoherence?.allInvariantsValid,\n temporalAlignmentValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.temporalAlignment?.valid,\n crossWindowGuardValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard?.valid,\n delayedTruthValid: unifiedState.epistemicTemporalCoherence?.invariants?.delayedTruthRule?.valid,\n reconciliationRequired:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard\n ?.reconciliation?.required\n },\n realityDriftObserver: {\n schema: unifiedState.realityDriftObserver?.schema,\n driftActive: unifiedState.realityDriftObserver?.driftObserverSummary?.active,\n misapprehensionShapeCount:\n unifiedState.realityDriftObserver?.misapprehensionShapeCatalog?.shapeCount,\n criticalShapeCount:\n unifiedState.realityDriftObserver?.driftObserverSummary?.criticalShapeCount,\n roboticsMismatchActive:\n unifiedState.realityDriftObserver?.roboticsFeedbackMismatch?.active\n },\n driftCausality: {\n schema: unifiedState.driftCausality?.schema,\n shapesWithCausalExplanation: unifiedState.driftCausality?.rdolLinkage?.shapesWithCausalExplanation,\n activeDomainCount: unifiedState.driftCausality?.causalitySummary?.activeDomainCount,\n propagationCausalityActive: unifiedState.driftCausality?.causalDomains?.propagation?.active\n },\n productReadiness: unifiedState.productReadiness,\n stateLayers: {\n raw: unifiedState.stateLayers.raw,\n derived: {\n systemState: unifiedState.stateLayers.derived.systemState,\n narrative: unifiedState.stateLayers.derived.narrative,\n validation: unifiedState.stateLayers.derived.validation\n },\n policy: {\n binding: unifiedState.stateLayers.policy.binding,\n decisionOwner: unifiedState.stateLayers.policy.decisionOwner,\n prohibitedActions: unifiedState.stateLayers.policy.prohibitedActions\n }\n },\n systemState: unifiedState.systemState,\n interpretation: unifiedState.interpretation,\n validation: {\n trustPosture: unifiedState.validation.trustPosture,\n adjustedConfidence: unifiedState.validation.adjustedConfidence,\n agreement: unifiedState.validation.confidenceDecomposition.agreement,\n divergenceScore: unifiedState.validation.divergence.divergenceScore,\n validated: unifiedState.validation.divergence.validated,\n uncertaintyTags: unifiedState.validation.uncertainty.tags.map((t) => t.id),\n flags: unifiedState.validation.divergence.flags.map((f) => f.id)\n },\n compression: unifiedState.compression\n }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 6 * 1024 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (\n !checkHttpRateLimit(`rhizoh_voice_transcribe:${rlKey}`, RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN, 60_000)\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const voiceEnv = rhizohVoiceTranscribeEnvV3();\n if (!voiceEnv.any) {\n sendJson(res, 503, {\n ok: false,\n error: \"voice_asr_not_configured\",\n env: voiceEnv\n });\n return;\n }\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n const result = await runRhizohVoiceTranscribeV3(\n applyLanguagePropagationToVoiceBodyV1(payload, langProp)\n );\n sendJson(res, result.ok ? 200 : 422, {\n ...result,\n traceId: String(payload?.traceId || \"\"),\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n atMs: Date.now()\n });\n } catch (error) {\n sendJson(res, 500, {\n ok: false,\n error: \"voice_transcribe_failed\",\n detail: String(error?.message || error),\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === rhizohRuntime.routes.rhizohLlm) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 128 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n logRhizohHealth(\"gateway_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: String(payload?.traceId || \"\"),\n auth: auth.ok ? \"ok\" : \"anon\",\n ip\n });\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (!checkHttpRateLimit(`rhizoh_llm:${rlKey}`, RL_RHIZOH_LLM_PER_MIN, 60_000)) {\n const rlTaxonomy = classifyStressResponseV0({ code: \"rate_limit_exceeded\" });\n return sendJson(res, 429, {\n ok: false,\n error: \"rate_limit_exceeded\",\n reply: rlTaxonomy.userMessageTr || \"İstek sınırı aşıldı. Kısa süre sonra tekrar deneyin.\",\n stressClass: rlTaxonomy.stressClass,\n responseAction: rlTaxonomy.responseAction,\n stressMatrix: rlTaxonomy.matrix,\n stressInterpretable: rlTaxonomy.interpretable,\n stressConfidence: rlTaxonomy.stressConfidence,\n actionConfidence: rlTaxonomy.actionConfidence,\n actionSoftened: rlTaxonomy.actionSoftened,\n responseActionStrict: rlTaxonomy.responseActionStrict,\n actionInterpretable: rlTaxonomy.actionInterpretable,\n conflictResolution: rlTaxonomy.conflictResolution,\n directive: \"NONE\"\n });\n }\n\n const requireExplicit = process.env.CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE === \"1\";\n const keyMode = normalizeClientLlmKeySource(payload?.llmKeySource ?? payload?.keySource);\n if (keyMode === null) {\n return sendJson(res, 400, {\n ok: false,\n error: \"invalid_llm_key_source\",\n reply: \"llmKeySource geçersiz. Kullanın: env | user_connection | auto\",\n directive: \"NONE\"\n });\n }\n if (requireExplicit && keyMode === \"auto\") {\n return sendJson(res, 400, {\n ok: false,\n error: \"llm_key_source_required\",\n reply: \"Sunucu CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE=1: istekte llmKeySource (env veya user_connection) zorunlu.\",\n directive: \"NONE\"\n });\n }\n\n const conn = auth.ok ? await resolveConnection(auth.uid, String(payload?.connectionId || \"\")) : null;\n\n if (keyMode === \"user_connection\") {\n if (!auth.ok) {\n return sendJson(res, 401, {\n ok: false,\n error: \"llm_auth_required_for_user_connection\",\n reply: \"Kayıtlı LLM bağlantısı için giriş gerekli.\",\n directive: \"NONE\"\n });\n }\n if (!conn?.apiKey) {\n return sendJson(res, 400, {\n ok: false,\n error: \"user_llm_connection_required\",\n reply: \"Geçerli bir LLM bağlantısı yok; Studio’da bağlantı ekleyin veya llmKeySource=env kullanın.\",\n directive: \"NONE\"\n });\n }\n }\n\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n let safePayload = applyLanguagePropagationToLlmPayloadV1({ ...(payload || {}) }, langProp);\n const trimOut = trimRhizohLlmGatewayPayloadV1(safePayload);\n if (trimOut.meta.trimmed) {\n logRhizohHealth(\"llm_payload_trimmed\", {\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n bytesBefore: trimOut.meta.bytesBefore,\n bytesAfter: trimOut.meta.bytesAfter\n });\n }\n safePayload = trimOut.payload;\n delete safePayload.apiKey;\n delete safePayload.llmKeySource;\n delete safePayload.keySource;\n\n let resolvedProvider = payload?.provider;\n let resolvedModel = payload?.model;\n let connApiKey = \"\";\n\n if (keyMode === \"user_connection\") {\n resolvedProvider = conn.provider;\n resolvedModel = conn.model;\n connApiKey = conn.apiKey || \"\";\n } else if (keyMode === \"env\") {\n connApiKey = \"\";\n } else {\n resolvedProvider = conn?.provider || payload?.provider;\n resolvedModel = conn?.model || payload?.model;\n connApiKey = conn?.apiKey || \"\";\n }\n\n const { result, traceId, turnLatencyMs, spinePhases, sampledTrace } = await rhizohGatewayTurn({\n safePayload,\n auth,\n keyMode,\n conn,\n resolvedProvider,\n resolvedModel,\n connApiKey\n });\n logRhizohHealth(\"llm_response\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId,\n turnLatencyMs: Number(turnLatencyMs || 0),\n keyMode\n });\n sendJson(res, 200, {\n ...result,\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n traceId,\n turnLatencyMs,\n ...(spinePhases ? { spinePhases } : {}),\n ...(sampledTrace !== undefined ? { sampledTrace } : {})\n });\n } catch (error) {\n const msg = String(error?.message || \"\");\n const code = error?.code || \"\";\n let status = 500;\n if (msg === \"payload_too_large\") status = 413;\n else if (msg === \"rate_limit_exceeded\") status = 429;\n else if (\n code === \"cost_hard_limit\" ||\n code === \"phased_rollout_capacity\" ||\n error?.containment === true ||\n String(code).startsWith(\"agent_\")\n ) {\n status = 429;\n } else if (code === \"user_soft_blocked\" || code === \"prompt_abuse_detected\") status = 403;\n else if ([\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(code) || msg.includes(\"missing_api_key_for_\"))\n status = 400;\n let rhizohFailureKind = \"provider_error\";\n if (msg === \"rate_limit_exceeded\" || status === 429) rhizohFailureKind = \"rate_limit\";\n else if (status === 403) rhizohFailureKind = \"policy_block\";\n else if (status === 400 || status === 413) rhizohFailureKind = \"client_config\";\n else if (msg.startsWith(\"provider_http_\")) {\n const st = Number(msg.replace(\"provider_http_\", \"\"));\n if (st === 408 || st === 504) rhizohFailureKind = \"timeout\";\n else rhizohFailureKind = \"provider_error\";\n } else if (/timeout|timed out|ETIMEDOUT/i.test(msg)) rhizohFailureKind = \"timeout\";\n\n const providerHttpMatch = msg.match(/^provider_http_(\\d{3})$/);\n const stressTaxonomy =\n error?.stressClass != null\n ? {\n stressClass: error.stressClass,\n responseAction: error.responseAction,\n stressMatrix: error.stressMatrix,\n stressInterpretable: error.stressInterpretable !== false,\n stressConfidence: error.stressConfidence,\n actionConfidence: error.actionConfidence,\n actionSoftened: error.actionSoftened,\n responseActionStrict: error.responseActionStrict,\n actionInterpretable: error.actionInterpretable,\n conflictResolution: error.conflictResolution,\n stressSecondary: error.stressSecondary,\n responseActions: error.responseActions\n }\n : (() => {\n const t = classifyStressResponseV0({\n code: code || msg,\n rhizohFailureKind,\n providerHttpStatus: providerHttpMatch\n ? Number(providerHttpMatch[1])\n : null\n });\n return {\n stressClass: t.stressClass,\n responseAction: t.responseAction,\n stressMatrix: t.matrix,\n stressInterpretable: t.interpretable,\n stressConfidence: t.stressConfidence,\n actionConfidence: t.actionConfidence,\n actionSoftened: t.actionSoftened,\n responseActionStrict: t.responseActionStrict,\n actionInterpretable: t.actionInterpretable,\n conflictResolution: t.conflictResolution,\n stressSecondary: t.stressSecondary,\n responseActions: t.responseActions\n };\n })();\n sendJson(res, status, {\n ok: false,\n rhizohFailureKind,\n ...(providerHttpMatch ? { providerHttpStatus: Number(providerHttpMatch[1]) } : {}),\n error: code || msg || \"rhizoh_llm_failed\",\n detail: msg || code || undefined,\n reply:\n error?.reply ||\n (status === 500 ? \"Rhizoh bağlantısı geçici olarak kesildi.\" : msg),\n directive: error?.directive || \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n ...stressTaxonomy\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicSeal) {\n try {\n if (!EPISTEMIC_SEAL_SECRET) {\n sendJson(res, 503, { ok: false, error: \"epistemic_seal_disabled\" });\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`epistemic_seal:${ip}`, RL_EPISTEMIC_SEAL_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const truth = body.truth_contract;\n if (!truth || typeof truth !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"truth_contract_required\" });\n return;\n }\n const sealBody = {\n truth_contract: truth,\n runtime_hash: String(body.runtime_hash || \"\"),\n model_route: {\n provider: body.model_route?.provider ?? null,\n model: body.model_route?.model ?? null\n },\n memory_digest: String(body.memory_digest || \"\"),\n world_snapshot_hash: String(body.world_snapshot_hash || \"\"),\n timestamp: Number(body.timestamp) || Date.now()\n };\n const canonical = canonicalEpistemicSealString(sealBody);\n const { hash, signature } = hashAndSignEpistemicSeal(canonical, EPISTEMIC_SEAL_SECRET);\n recordGenesisEpistemicSealIssued(hash);\n const attestation = {\n schema: EPISTEMIC_SEAL_SCHEMA,\n algorithm: \"SHA-256+HMAC-SHA256\",\n keyHint: process.env.CASTLE_EPISTEMIC_SEAL_SECRET ? \"CASTLE_EPISTEMIC_SEAL_SECRET\" : \"CASTLE_GATEWAY_TOKEN\",\n issuedAt: Date.now(),\n gateway: { service: \"castle-gateway\", listenPort: PORT }\n };\n sendJson(res, 200, {\n ok: true,\n hash,\n signature,\n attestation,\n canonicalBytes: Buffer.byteLength(canonical, \"utf8\")\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"seal_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicLogsBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`epistemic_logs:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 120) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const persisted = await persistEpistemicLedgerBatch(auth.uid, entries);\n recordGenesisEpistemicLedgerPersisted(persisted.persisted);\n // Internal cognition layer: forecast stays gateway/ledger-only (no client exposure).\n try {\n await persistEpistemicForecastBatch(auth.uid, persisted.normalized || []);\n } catch {\n /* forecast failures must not break primary ledger ingest */\n }\n sendJson(res, 200, {\n ok: true,\n persisted: persisted.persisted,\n mode: persisted.mode,\n latest: persisted.normalized?.[persisted.normalized.length - 1] || null\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"epistemic_log_ingest_failed\") });\n }\n return;\n }\n\n sendJson(res, 404, { ok: false, error: \"not_found\" });\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\nspiralWssForGenesis = wss;\nconst orchestrator = createOrchestrator();\nconst clientStats = new Map();\nlet broadcasterClientId = null;\nconst spiralState = {\n rooms: [{ id: \"room-default\", name: \"Spiral Default Arena\" }],\n activeByClient: new Map(),\n characters: new Map() // id -> {id,name,role,roomId,source}\n};\n\n/** Castle social rooms — keyed by `castleRoomKey` from {@link WS_MESSAGE.CASTLE_SOCIAL_PULSE}. */\nconst castleSocialRoomState = new Map();\nconst CASTLE_SOCIAL_MEMBER_TTL_MS = 90_000;\nconst CASTLE_SOCIAL_MAX_PULSE_BYTES = 8192;\n\nfunction removeCastleSocialClientFromAllRooms(clientId) {\n const cid = String(clientId || \"\");\n if (!cid) return;\n for (const room of castleSocialRoomState.values()) {\n room.members.delete(cid);\n }\n}\n\nfunction buildCastleSocialRoomPayload(roomKey) {\n const room = castleSocialRoomState.get(roomKey);\n if (!room) return null;\n const now = Date.now();\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const roster = [...room.members.values()].map((m) => ({\n userId: m.userId,\n lastMs: m.lastMs,\n ...m.sp\n }));\n const peerFeeds =\n room.walPeers && room.walPeers.size\n ? [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }))\n : [];\n return { castleRoomKey: roomKey, seq: room.seq, roster, peerFeeds, ts: now };\n}\n\nconst CASTLE_WAL_PEER_MAX_FEED_BYTES = 48 * 1024;\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {{ ok: boolean, code?: string, reason?: string }} gate\n */\nfunction rejectWalPeerIngressV0(socket, gate) {\n const code = gate.code || \"wal_peer_rejected\";\n recordWalPeerRejectV0(code);\n recordSubstrateOtelEventV0(\"wal_peer_reject\", { code, reason: String(gate.reason || \"\").slice(0, 120) });\n socket.send(\n JSON.stringify(\n createEnvelope(WS_MESSAGE.ERROR, {\n error: gate.reason || \"WAL peer ingress rejected.\",\n code\n })\n )\n );\n}\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {Record<string, unknown> | null | undefined} walPeerFeed\n * @returns {boolean}\n */\nfunction gateWalPeerIngressV0(socket, walPeerFeed) {\n const authGate = assertWalPeerSocketAuthorityV0(socket);\n if (!authGate.ok) {\n rejectWalPeerIngressV0(socket, authGate);\n return false;\n }\n const sigGate = validateWalPeerFeedSignatureV0(\n walPeerFeed && typeof walPeerFeed === \"object\" ? walPeerFeed : {}\n );\n if (!sigGate.ok) {\n rejectWalPeerIngressV0(socket, sigGate);\n return false;\n }\n return true;\n}\n\nfunction handleCastleWalPeerFeed(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_WAL_PEER_MAX_FEED_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_WAL_PEER_FEED too large.\" })));\n return;\n }\n const walPeerFeed = p.walPeerFeed && typeof p.walPeerFeed === \"object\" ? p.walPeerFeed : {};\n if (!gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const castleRoomKey = String(p.castleRoomKey || \"wal:convergence\").slice(0, 64);\n const castleId = String(p.castleId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId,\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: room.members.get(socket.clientId)?.sp ?? {}\n });\n const peerFeeds = [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }));\n const body = {\n castleRoomKey,\n seq: room.seq,\n peerFeeds,\n ts: now\n };\n const env = createEnvelope(WS_MESSAGE.CASTLE_WAL_PEER_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys(), ...room.walPeers.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction handleCastleSocialPulse(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_SOCIAL_MAX_PULSE_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_SOCIAL_PULSE too large.\" })));\n return;\n }\n const castleRoomKey = String(p.castleRoomKey || \"default\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n const sp = p.socialPulse && typeof p.socialPulse === \"object\" ? p.socialPulse : {};\n const walPeerFeed =\n p.walPeerFeed && typeof p.walPeerFeed === \"object\"\n ? p.walPeerFeed\n : sp.walPeerFeed && typeof sp.walPeerFeed === \"object\"\n ? sp.walPeerFeed\n : null;\n if (walPeerFeed && !gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const spNext = walPeerFeed ? { ...sp, walPeerFeed } : { ...sp };\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: spNext\n });\n if (walPeerFeed) {\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId: String(p.castleId || userId).slice(0, 64),\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n }\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const body = buildCastleSocialRoomPayload(castleRoomKey);\n if (!body) return;\n const env = createEnvelope(WS_MESSAGE.CASTLE_SOCIAL_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcast(msgObj) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcastState() {\n broadcast(\n createEnvelope(WS_MESSAGE.BROADCAST_STATE, {\n broadcasterClientId,\n viewers: [...wss.clients].filter((c) => c.readyState === 1).length\n })\n );\n}\n\nfunction currentSpiralStatePayload() {\n return {\n rooms: spiralState.rooms,\n characters: [...spiralState.characters.values()]\n };\n}\n\nfunction broadcastSpiralState() {\n broadcast(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload()));\n}\n\nfunction ensureRoom(roomId, fallbackName = null) {\n if (!roomId) return spiralState.rooms[0].id;\n const exists = spiralState.rooms.find((r) => r.id === roomId);\n if (exists) return exists.id;\n const created = { id: roomId, name: fallbackName || `Spiral-${roomId.slice(-4)}` };\n spiralState.rooms.push(created);\n return created.id;\n}\n\nfunction syncSpiralCharactersWithWorld(snapshot) {\n const agents = snapshot?.agents || [];\n for (const a of agents) {\n if (!spiralState.characters.has(a.id)) {\n spiralState.characters.set(a.id, {\n id: a.id,\n name: a.id,\n role: \"agent\",\n roomId: spiralState.rooms[0].id,\n source: \"world\"\n });\n }\n }\n}\n\nfunction broadcastExcept(msgObj, exceptSocket) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client !== exceptSocket && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction validateInputFrame(payload, socket) {\n const stats = clientStats.get(socket.clientId) || { lastFrame: 0, windowStart: Date.now(), msgCount: 0 };\n const now = Date.now();\n if (now - stats.windowStart > 5000) {\n stats.windowStart = now;\n stats.msgCount = 0;\n }\n stats.msgCount += 1;\n clientStats.set(socket.clientId, stats);\n if (stats.msgCount > 120) return \"Rate limit exceeded.\";\n\n if (!payload || !Number.isInteger(payload.frame)) return \"frame must be integer.\";\n if (payload.frame <= stats.lastFrame) return \"frame must be monotonic.\";\n const commands = Array.isArray(payload.commands) ? payload.commands : [];\n if (commands.length > 8) return \"Too many commands in one frame.\";\n for (const cmd of commands) {\n if (!cmd || typeof cmd.command !== \"string\") return \"Invalid command format.\";\n if (\n cmd.command !== \"SPAWN_AGENT\" &&\n cmd.command !== \"LIST_CASTLES\" &&\n cmd.command !== \"SPAWN_ENTITY\" &&\n cmd.command !== \"CREATE_CASTLE\"\n ) return \"Unsupported command.\";\n }\n stats.lastFrame = payload.frame;\n clientStats.set(socket.clientId, stats);\n return null;\n}\n\nfunction validateSignalPayload(payload) {\n if (!payload || typeof payload !== \"object\") return \"Invalid signaling payload.\";\n const signalType = payload.signalType;\n if (![\"OFFER\", \"ANSWER\", \"ICE\"].includes(signalType)) return \"Invalid signal type.\";\n if (typeof payload.to !== \"string\" || payload.to.length < 3) return \"Invalid signal destination.\";\n if (signalType !== \"ICE\" && JSON.stringify(payload.sdp || {}).length > 16 * 1024) return \"SDP too large.\";\n if (signalType === \"ICE\" && JSON.stringify(payload.candidate || {}).length > 6 * 1024) return \"ICE too large.\";\n return null;\n}\n\nfunction sanitizeRawMessage(raw) {\n if (!raw) return null;\n const text = raw.toString();\n if (Buffer.byteLength(text, \"utf8\") > MAX_MESSAGE_BYTES) return null;\n return text;\n}\n\nwss.on(\"connection\", async (socket, req) => {\n const origin = req?.headers?.origin || \"\";\n if (!httpCors.isWebSocketOriginAllowed(origin)) {\n socket.close(1008, \"Origin not allowed\");\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const path = req?.url || \"\";\n const query = new URL(`ws://localhost${path}`).searchParams;\n const token = query.get(\"token\");\n if (!token || token !== REQUIRED_GATEWAY_TOKEN) {\n socket.close(1008, \"Invalid gateway token\");\n return;\n }\n }\n if (REQUIRE_AUTH) {\n const result = await verifyClientToken(req);\n if (!result.ok) {\n const isLocal = origin.includes(\"localhost\") || origin.includes(\"127.0.0.1\") || !origin;\n if (!(ALLOW_DEV_ANON && isLocal)) {\n socket.close(1008, result.reason);\n return;\n }\n socket.auth = { ok: true, kind: \"dev-anon\", user: { uid: \"dev-anon\" } };\n } else {\n socket.auth = result;\n }\n }\n\n socket.clientId = `c-${Math.random().toString(36).slice(2, 8)}`;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.HELLO, { message: \"Castle Gateway online.\", clientId: socket.clientId })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.WORLD_SNAPSHOT, orchestrator.worldSnapshot())));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.BROADCAST_STATE, { broadcasterClientId, viewers: [...wss.clients].filter((c) => c.readyState === 1).length })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload())));\n broadcastExcept(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }), socket);\n\n socket.on(\"message\", (raw) => {\n const safeRaw = sanitizeRawMessage(raw);\n if (!safeRaw) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Message too large or invalid.\" })));\n return;\n }\n const parsed = safeJsonParse(safeRaw);\n if (!parsed?.type) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid message.\" })));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND_TEXT) {\n const canonical = parseTextToCommand(parsed.payload?.text);\n if (!canonical) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Unknown command text.\" })));\n return;\n }\n const result = orchestrator.applyCommand(canonical);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND) {\n const result = orchestrator.applyCommand(parsed.payload);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n }\n\n if (parsed.type === WS_MESSAGE.INPUT_FRAME) {\n const err = validateInputFrame(parsed.payload, socket);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const frame = parsed.payload.frame;\n const commands = Array.isArray(parsed.payload?.commands) ? parsed.payload.commands : [];\n orchestrator.queueInputFrame(socket.clientId, frame, commands);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, queued: true, frame })));\n }\n\n if (parsed.type === WS_MESSAGE.SIGNAL) {\n const payload = parsed.payload || {};\n const err = validateSignalPayload(payload);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const signalType = payload.signalType;\n const to = payload.to;\n for (const client of wss.clients) {\n if (client.clientId === to && client.readyState === 1) {\n client.send(JSON.stringify(createEnvelope(WS_MESSAGE.SIGNAL, { ...payload, from: socket.clientId })));\n break;\n }\n }\n }\n\n if (parsed.type === WS_MESSAGE.DRONE_TELEMETRY) {\n const payload = parsed.payload || {};\n const json = JSON.stringify(payload);\n if (Buffer.byteLength(json, \"utf8\") > 48 * 1024) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"DRONE_TELEMETRY payload too large.\" })));\n return;\n }\n broadcastExcept(createEnvelope(WS_MESSAGE.DRONE_TELEMETRY, { ...payload, relayFrom: socket.clientId }), socket);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_SOCIAL_PULSE) {\n handleCastleSocialPulse(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_WAL_PEER_FEED) {\n handleCastleWalPeerFeed(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.OPEN_DATA_QUERY) {\n queryOpenData(parsed.payload)\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: true, result })));\n })\n .catch((e) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: false, error: e.message })));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_QUERY) {\n runRhizohBrainV2({\n ...(parsed.payload || {}),\n uid: socket?.auth?.user?.uid || \"unknown\"\n })\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, result)));\n })\n .catch(async () => {\n const fallback = await runRhizohBrain(parsed.payload || {}).catch(() => ({\n ok: false,\n reply: \"Rhizoh temporary disturbance. Habitat remains stable.\"\n }));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, fallback)));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.BROADCAST_REGISTER) {\n const role = String(parsed.payload?.role || \"\");\n if (role === \"GENESIS_BROADCAST_AGENT\") {\n broadcasterClientId = socket.clientId;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, registered: role })));\n broadcastState();\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.STUDIO_CUE) {\n // Only registered broadcaster can push global cues.\n if (!broadcasterClientId || socket.clientId !== broadcasterClientId) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Only broadcaster agent can emit STUDIO_CUE.\" })));\n return;\n }\n const cue = parsed.payload || {};\n if (typeof cue !== \"object\") {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid STUDIO_CUE payload.\" })));\n return;\n }\n broadcast(createEnvelope(WS_MESSAGE.STUDIO_CUE, cue));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_JOIN_ROOM) {\n const requested = String(parsed.payload?.roomId || \"\").trim();\n const requestedName = String(parsed.payload?.roomName || \"\").trim();\n const roomId = ensureRoom(requested || `room-${Math.random().toString(36).slice(2, 7)}`, requestedName || null);\n spiralState.activeByClient.set(socket.clientId, roomId);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, roomId })));\n broadcastSpiralState();\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_CREATE_CHARACTER) {\n const name = String(parsed.payload?.name || \"\").trim().slice(0, 32) || `AI-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;\n const role = String(parsed.payload?.role || \"agent\").trim().slice(0, 24) || \"agent\";\n const roomId = ensureRoom(\n String(parsed.payload?.roomId || spiralState.activeByClient.get(socket.clientId) || spiralState.rooms[0].id),\n null\n );\n\n // Character creation always maps to an authoritative world agent.\n const result = orchestrator.applyCommand({\n command: COMMAND.SPAWN_AGENT,\n params: { targetCastleId: \"castle-01\" }\n });\n if (!result?.ok || !result?.data?.id) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Character creation failed.\" })));\n return;\n }\n const id = result.data.id;\n spiralState.characters.set(id, { id, name, role, roomId, source: \"spawned\" });\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, characterId: id, roomId })));\n broadcastSpiralState();\n return;\n }\n });\n\n socket.on(\"close\", () => {\n if (socket.clientId === broadcasterClientId) broadcasterClientId = null;\n clientStats.delete(socket.clientId);\n spiralState.activeByClient.delete(socket.clientId);\n removeCastleSocialClientFromAllRooms(socket.clientId);\n broadcast(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }));\n broadcastState();\n });\n});\n\nsetInterval(() => {\n void (async () => {\n try {\n const { frame, snapshot, delta, packedDelta } = orchestrator.tick(1 / 20);\n syncSpiralCharactersWithWorld(snapshot);\n broadcast(createEnvelope(WS_MESSAGE.NETWORK_TICK, { frame }));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_TICK, snapshot));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA, delta));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA_PACKED, packedDelta));\n broadcastSpiralState();\n await runAcademyEventTick();\n const executed = await tickAutomations(Date.now());\n for (const a of executed) {\n await addTranscript(a.uid, {\n source: \"planner\",\n eventType: \"automation-executed\",\n text: `Automation executed: ${a.title} (${a.action})`,\n roomId: \"studio-main\",\n meta: { automationId: a.id, action: a.action, deviceId: a.deviceId || \"\" }\n });\n }\n } catch (e) {\n console.error(\"[GATEWAY] world tick / automations loop error:\", String(e?.message || e));\n }\n })();\n}, 50);\n\n(async () => {\n await rhizohRuntime.telemetry.initOpenTelemetry();\n if (String(process.env.CASTLE_GENESIS_DISK_PERSIST ?? \"\").trim() === \"\") {\n process.env.CASTLE_GENESIS_DISK_PERSIST = \"1\";\n }\n const genesisDiskBoot = await hydrateGenesisContinuityPersistenceBootV0();\n if (!genesisDiskBoot.ok) {\n console.warn(\"[GATEWAY] genesis disk hydrate failed; continuing with in-memory genesis continuity\");\n }\n httpServer.listen(PORT, () => {\n startGenesisCanonicalClock();\n installGenesisCheckpointSurfaceGetter(buildGenesisRuntimeSurfacePayloadLive);\n setGenesisContinuityAfterPublishHook((seq) => {\n noteGenesisCheckpointSeqCommitted(seq);\n noteGenesisContinuitySeqAcceptedV0(seq);\n });\n startGenesisContinuityInfraSampler(buildGenesisRuntimeSurfacePayloadLive, 2000);\n logProductionObservatorySurfaceGuardsV0();\n logGatewaySubstrateAuthorityGuardsV0();\n console.log(`[GATEWAY] ws/http://localhost:${PORT}`);\n console.log(\n `[GATEWAY] genesis continuity: GET ${rhizohRuntime.routes.genesisRuntime} | POST ${rhizohRuntime.routes.genesisIngress} | SSE ${rhizohRuntime.routes.genesisStream} | replay ${rhizohRuntime.routes.genesisReplay} | diff ${rhizohRuntime.routes.genesisReplayDiff} | equiv ${rhizohRuntime.routes.genesisReplayEquivalence} | analytics ${rhizohRuntime.routes.genesisReplayAnalytics} | evolution ${rhizohRuntime.routes.genesisReplayEvolution}`\n );\n if (String(process.env.CASTLE_GENESIS_OBSERVATORY_LIVE_V01 ?? \"\").trim() === \"1\") {\n console.log(\"GENESIS OBSERVATORY LIVE v0.1 — READ ONLY PHASE STARTED\");\n }\n });\n})().catch((e) => {\n console.error(\"[GATEWAY] fatal boot (before or during listen):\", e);\n process.exit(1);\n});\n"}, "74b86811ac63eeb1c480ccc1f4e14301ef913af1": {"t": 1785754160.244235, "n": 154961, "h": "5aba8dd93d177032", "c": "import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport dotenv from \"dotenv\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconsole.log(\"🔥 [GENESIS_BOOT] entry file:\", __filename);\nconsole.log(\"🔥 [GENESIS_BOOT] node env:\", process.env.NODE_ENV);\nconsole.log(\"🔥 [GENESIS_BOOT] pid:\", process.pid);\nconsole.log(\"🧬 [GENESIS_BOOT] cwd:\", process.cwd());\nconsole.log(\"🧬 [GENESIS_BOOT] import.meta.url:\", import.meta.url);\ndotenv.config({ path: path.join(__dirname, \"..\", \".env\") });\ndotenv.config({ path: path.join(__dirname, \"..\", \".env.local\"), override: true });\n\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport { createServer } from \"node:http\";\nimport { createOrchestrator } from \"@castle/orchestrator\";\nimport { parseTextToCommand } from \"@castle/command-dsl\";\nimport { WS_MESSAGE, COMMAND, createEnvelope, safeJsonParse } from \"@castle/protocol\";\nimport { queryOpenData } from \"./openData.js\";\nimport { verifyClientToken } from \"./auth.js\";\nimport { runRhizohBrain } from \"./rhizohBrain.js\";\nimport { queryRhizohLlm } from \"./rhizohLlmGateway.js\";\nimport { rhizohGatewayTurn } from \"./rhizohGatewayTurn.js\";\nimport {\n RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n runRhizohVoiceTranscribeV3,\n rhizohVoiceTranscribeEnvV3\n} from \"./rhizohVoiceTranscribeV3.js\";\nimport { createHttpCorsPolicy, normalizeHttpOrigin, parseAllowedOriginsFromEnv } from \"./httpCorsPolicyV1.js\";\nimport {\n applyLanguagePropagationToLlmPayloadV1,\n applyLanguagePropagationToVoiceBodyV1,\n buildRhizohLanguagePropagationEchoV1,\n parseRhizohLanguagePropagationV1\n} from \"./rhizohLanguagePropagationV1.js\";\nimport {\n meshAppendDelta,\n meshContinuityAggregate,\n meshJoin,\n meshLeave,\n meshReplayFromNodeId,\n meshReplayFromSeq,\n meshSnapshot,\n meshSubscribeSse\n} from \"./presenceRoomMesh.js\";\nimport { runRhizohBrainV2 } from \"./rhizohBrainV2.js\";\nimport {\n listConnections,\n createConnection,\n updateConnection,\n deleteConnection,\n setDefaultConnection,\n resolveConnection\n} from \"./llmConnectionsStore.js\";\nimport { checkHttpRateLimit, getHttpClientIp } from \"./castleHttpRateLimit.js\";\nimport {\n submitAbuseReportV0,\n listModerationQueueV0,\n softBlockUserV0,\n removeSoftBlockV0,\n listSoftBlockedUsersV0\n} from \"./ops/moderationMvpV0.js\";\nimport { listRecentAgentSnapshotsV0 } from \"./ops/agentObservabilityV0.js\";\nimport { readAgentContainmentConfigV0 } from \"./ops/agentContainmentV0.js\";\nimport { readCostContainmentConfigV0, getCostContainmentStatsV0 } from \"./ops/costContainmentV0.js\";\nimport { getGlobalCostLedgerSnapshotV0, getGclBackendStatusV0 } from \"./ops/globalCostLedgerV0.js\";\nimport { evaluateEconomicStrategyL3V0 } from \"./ops/economicStrategyEngineL3V0.js\";\nimport { getPhasedRolloutStatsV0, readPhasedRolloutTierV0 } from \"./ops/phasedRolloutV0.js\";\nimport { buildUnifiedStateNarrativeV0 } from \"./ops/unifiedStateNarrativeV0.js\";\nimport { resolveHardeningTenantIdV0 } from \"./ops/narrativeTenantIsolationV0.js\";\nimport {\n classifyStressResponseV0,\n verifyStressTaxonomyCoverageV0,\n verifyStressConflictResolutionV0\n} from \"./ops/stressResponseTaxonomyV0.js\";\nimport { verifyResolutionStabilityEnvelopeV0 } from \"./ops/resolutionStabilityEnvelopeV0.js\";\nimport { logLlmAccess } from \"./castleLlmAudit.js\";\nimport { appendMemory, listMemories, getMemoryContext, getPersonaGoalMemory, setPersonaGoalMemory, autoCompactMemories } from \"./memoryStore.js\";\nimport { getFirebasePersistence } from \"./firebasePersistence.js\";\nimport { registerAgentIdentity, listAgentIdentities, getAgentIdentity, updateAgentIdentity } from \"./agentIdentityStore.js\";\nimport { queueAcademyEvent, listAcademyEvents, resolveAcademyEvent, runAcademyEventTick } from \"./academyEventEngine.js\";\nimport { enqueuePdfJob, getPdfJob } from \"./pdfPipeline.js\";\nimport {\n createPublishSession,\n listPublishSessions,\n updatePublishSession,\n addTranscript,\n listTranscripts,\n searchMetadata,\n registerDevice,\n listDevices,\n updateDevice,\n appendTelemetry,\n listTelemetry,\n createAutomation,\n listAutomations,\n tickAutomations,\n enqueueCommand,\n listCommands,\n updateCommandStatus,\n registerSocialChannel,\n listSocialChannels\n} from \"./studioOpsStore.js\";\nimport { metrics as infraMetrics } from \"./infra/metrics.js\";\nimport { renderRhizohPrometheusMetrics, rhizohEnterpriseMetrics } from \"./infra/rhizohEnterpriseMetrics.js\";\nimport { scoreHealth } from \"./infra/healthScore.js\";\nimport { getTraceById, listTracesBySession } from \"./infra/traceRegistry.js\";\nimport { canonicalEpistemicSealString, hashAndSignEpistemicSeal, EPISTEMIC_SEAL_SCHEMA } from \"./epistemicSeal.js\";\nimport { persistEpistemicLedgerBatch } from \"./epistemicLedgerStore.js\";\nimport { persistEpistemicForecastBatch } from \"./epistemicForecastStore.js\";\nimport { buildRhizohExternalGroundTruthPayload } from \"./rhizohExternalGroundTruthGateway.js\";\nimport { ingestRhizohExternalLossBatchHttp } from \"./rhizohExternalLossIngestGateway.js\";\nimport {\n ingestRhizohProductOutcomeHttp,\n listRhizohProductOutcomeAggregates,\n verifyRhizohOutcomeHmac,\n verifyRhizohOutcomeSourceToken\n} from \"./rhizohProductOutcomeIngestGateway.js\";\nimport { initRhizoh } from \"./rhizohProductionBootstrap.js\";\nimport { handleAcademicObservatoryExportGetV0 } from \"./rhizoh/academicObservatoryHttpV0.js\";\nimport {\n buildGenesisRuntimeSurfacePayload,\n recordGenesisEpistemicLedgerPersisted,\n recordGenesisEpistemicSealIssued,\n startGenesisCanonicalClock\n} from \"./genesisRuntimeSurfaceV0.js\";\nimport { registerGenesisSseClient, getGenesisContinuitySeq, setGenesisContinuityAfterPublishHook } from \"./genesisContinuityStreamHubV0.js\";\nimport { computeGenesisReplayFingerprintV0 } from \"./genesisReplayFingerprintV0.js\";\nimport { buildGenesisGatewayCapabilitiesV0 } from \"./genesisGatewayCapabilitiesV0.js\";\nimport {\n installGenesisCheckpointSurfaceGetter,\n noteGenesisCheckpointSeqCommitted,\n getLatestGenesisSignedCheckpoint\n} from \"./genesisContinuityCheckpointV0.js\";\nimport { hydrateGenesisContinuityPersistenceBootV0 } from \"./genesisContinuityHydrateBootV0.js\";\nimport { startGenesisContinuityInfraSampler } from \"./genesisContinuityInfraSamplerV0.js\";\nimport {\n genesisCheckpointQueryBySeqV0,\n genesisCheckpointQueryRangeV0,\n genesisCheckpointQueryLineageV0\n} from \"./genesisCheckpointQueryV0.js\";\nimport {\n genesisContinuityEventArchiveEnabled,\n queryGenesisContinuityEventArchiveV0,\n GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION\n} from \"./genesisContinuityEventArchiveV0.js\";\nimport { resolveGenesisReplayRouterV1, GENESIS_REPLAY_ROUTER_SCHEMA } from \"./genesisReplayRouterV1.js\";\nimport {\n computeGenesisReplayTemporalDiffV1,\n GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA\n} from \"./genesisReplayTemporalDiffV1.js\";\nimport { compareGenesisReplayEquivalenceV1 } from \"./genesisReplayEquivalenceV1.js\";\nimport { computeGenesisReplayAnalyticsV1 } from \"./genesisReplayAnalyticsV1.js\";\nimport { computeGenesisReplayEvolutionV1 } from \"./genesisReplayEvolutionV1.js\";\nimport { handleStripeCheckoutCreate, handleStripeMembershipWebhook } from \"./stripeWebhookMembership.js\";\nimport { buildRealityHealthPayload } from \"./edgeRealityConsistencyV1.js\";\nimport {\n assertWalPeerSocketAuthorityV0,\n buildGatewaySubstrateAuthoritySnapshotV0,\n logGatewaySubstrateAuthorityGuardsV0,\n validateWalPeerFeedSignatureV0\n} from \"./gatewaySubstrateAuthorityV0.js\";\nimport {\n buildSubstrateOperationalSnapshotV0,\n recordWalPeerRejectV0,\n renderSubstratePrometheusMetricsV0\n} from \"./infra/substrateOperationalMetrics.js\";\nimport { ingestClientSubstrateHealthV0 } from \"./rhizohSubstrateHealthIngestV0.js\";\nimport { recordSubstrateOtelEventV0 } from \"./infra/opentelemetrySubstrateV0.js\";\nimport {\n GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n getGenesisWorldObservationIngressStatusV0,\n ingestGenesisWorldObservationV0\n} from \"./genesisContinuityClientIngressV0.js\";\nimport {\n getGenesisContinuitySeqAuditV0,\n noteGenesisContinuitySeqAcceptedV0\n} from \"./genesisContinuitySeqGapV0.js\";\n\n/** Render / Fly / Railway: `PORT` — yoksa `CASTLE_GATEWAY_PORT` — yoksa 8090. */\nconst PORT =\n Number(process.env.PORT) ||\n Number(process.env.CASTLE_GATEWAY_PORT) ||\n 8090;\n/** Spiral / world WebSocket server — set after `wss` init; used by genesis runtime surface. */\nlet spiralWssForGenesis = null;\nconst MAX_MESSAGE_BYTES = Number(process.env.CASTLE_MAX_MESSAGE_BYTES || 32 * 1024);\nconst REQUIRED_GATEWAY_TOKEN = process.env.CASTLE_GATEWAY_TOKEN || \"\";\nconst ALLOWED_ORIGINS = parseAllowedOriginsFromEnv().fromList;\nconst httpCors = createHttpCorsPolicy();\n\n/** @returns {string|null} `Access-Control-Allow-Origin`; null = browser CORS fail. */\nfunction accessControlAllowOriginValue(req) {\n return httpCors.accessControlAllowOriginValue(req);\n}\n\nfunction applyHttpCorsHeaders(req, res) {\n return httpCors.applyHttpCorsHeaders(req, res);\n}\nconst REQUIRE_AUTH = process.env.CASTLE_REQUIRE_AUTH === \"true\";\nconst ALLOW_DEV_ANON = process.env.CASTLE_ALLOW_DEV_ANON !== \"false\";\nconst ALLOW_DEV_HTTP_UID = process.env.CASTLE_ALLOW_DEV_HTTP_UID !== \"false\";\n/** Presence mesh: allow `X-Castle-Guest-Id` when Firebase auth absent (default on). Set `false` to require auth only. */\nconst ALLOW_MESH_GUEST = process.env.CASTLE_ALLOW_MESH_GUEST !== \"false\";\nconst SOCIAL_RETRY_MAX = Math.max(0, Math.min(5, Number(process.env.CASTLE_SOCIAL_RETRY_MAX || 2)));\nconst SOCIAL_RETRY_BASE_MS = Math.max(100, Number(process.env.CASTLE_SOCIAL_RETRY_BASE_MS || 700));\nconst TELEGRAM_BOT_TOKEN = process.env.CASTLE_TELEGRAM_BOT_TOKEN || \"\";\nconst WHATSAPP_TOKEN = process.env.CASTLE_WHATSAPP_TOKEN || \"\";\nconst WHATSAPP_PHONE_NUMBER_ID = process.env.CASTLE_WHATSAPP_PHONE_NUMBER_ID || \"\";\nconst RL_RHIZOH_LLM_PER_MIN = Math.max(5, Number(process.env.CASTLE_RL_RHIZOH_LLM_PER_MIN || 40));\nconst RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN || 30)\n);\nconst RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN || 120));\nconst RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN = Math.max(8, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN || 48));\nconst RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN = Math.max(4, Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN || 24));\nconst RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN || 18)\n);\nconst RL_EPISTEMIC_SEAL_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_EPISTEMIC_SEAL_PER_MIN || 120));\nconst RL_EPISTEMIC_LOG_PER_MIN = Math.max(20, Number(process.env.CASTLE_RL_EPISTEMIC_LOG_PER_MIN || 240));\nconst EPISTEMIC_SEAL_SECRET = String(\n process.env.CASTLE_EPISTEMIC_SEAL_SECRET || process.env.CASTLE_GATEWAY_TOKEN || \"\"\n).trim();\nconst RL_LLM_CONN_TEST_PER_MIN = Math.max(2, Number(process.env.CASTLE_RL_LLM_CONNECTION_TEST_PER_MIN || 8));\n\n/** One-shot production warnings: CORS surface, auth toggle, dev bypass flags (observatory deploy hygiene). */\nfunction logProductionObservatorySurfaceGuardsV0() {\n if (String(process.env.NODE_ENV || \"\").toLowerCase() !== \"production\") return;\n const tag = \"[GATEWAY_PROD_GUARD]\";\n const primaryCors = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n const corsListEmpty = ALLOWED_ORIGINS.length === 0;\n if (corsListEmpty && !primaryCors) {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty and CASTLE_HTTP_CORS_ORIGIN empty — production still merges RHIZOH_BROWSER_ORIGINS_V1 defaults; set env for preview/staging hosts`\n );\n } else if (corsListEmpty && primaryCors && primaryCors !== \"*\") {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty — only CASTLE_HTTP_CORS_ORIGIN set; add web.app + firebaseapp.com (and previews) to CASTLE_ALLOWED_ORIGINS if clients use alternate hostnames`\n );\n }\n if (primaryCors && /localhost|127\\.0\\.0\\.1/i.test(primaryCors)) {\n console.warn(`${tag} CORS: CASTLE_HTTP_CORS_ORIGIN is localhost while NODE_ENV=production`);\n }\n if (REQUIRE_AUTH) {\n console.warn(\n `${tag} AUTH: CASTLE_REQUIRE_AUTH=true — clients must send a valid gateway credential (otherwise silent 401 on replay/SSE); EventSource cannot set custom headers unless proxied`\n );\n if (REQUIRED_GATEWAY_TOKEN.length < 16) {\n console.warn(`${tag} AUTH: CASTLE_GATEWAY_TOKEN missing or short while require-auth enabled`);\n }\n }\n if (ALLOW_DEV_ANON) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_ANON is not \"false\" — dev anonymous path may be live in production (set CASTLE_ALLOW_DEV_ANON=false)`\n );\n }\n if (ALLOW_DEV_HTTP_UID) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_HTTP_UID is not \"false\" — X-Castle-Dev-Uid bypass risk (set CASTLE_ALLOW_DEV_HTTP_UID=false)`\n );\n }\n}\n\nconst rhizohRuntime = initRhizoh();\n\n/** İstemci: llmKeySource veya keySource — env | user_connection | auto */\nfunction normalizeClientLlmKeySource(raw) {\n const s = String(raw ?? \"\").trim().toLowerCase();\n if (s === \"env\" || s === \"server\") return \"env\";\n if (s === \"user_connection\" || s === \"connection\" || s === \"user\") return \"user_connection\";\n if (s === \"auto\" || s === \"\") return \"auto\";\n return null;\n}\n\nfunction sendJson(res, statusCode, payload) {\n res.writeHead(statusCode, { \"Content-Type\": \"application/json; charset=utf-8\" });\n res.end(JSON.stringify(payload));\n}\n\nfunction logRhizohHealth(stage, detail = {}) {\n try {\n console.info(`[CASTLE_${String(stage || \"unknown\")}]`, detail && typeof detail === \"object\" ? detail : {});\n } catch {\n /* noop */\n }\n}\n\nfunction renderInfraMetricsProm() {\n const lines = [\n `castle_gateway_events_processed_total ${infraMetrics.eventsProcessed}`,\n `castle_gateway_duplicate_reject_total ${infraMetrics.duplicateRejects}`,\n `castle_gateway_enqueue_latency_ms ${infraMetrics.enqueueLatencyMs}`,\n `castle_gateway_queue_depth ${infraMetrics.queueDepth}`,\n `castle_gateway_queue_lag_seconds ${(Number(infraMetrics.queueLag || 0) / 1000).toFixed(3)}`,\n `castle_gateway_errors_total ${infraMetrics.errors}`\n ];\n const buckets = infraMetrics.enqueueLatencyBuckets || {};\n for (const k of Object.keys(buckets)) {\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"${k}\"} ${buckets[k]}`);\n }\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"+Inf\"} ${infraMetrics.eventsProcessed}`);\n if (process.env.CASTLE_SUBSTRATE_METRICS !== \"0\") {\n lines.push(renderSubstratePrometheusMetricsV0().trimEnd());\n }\n return lines.join(\"\\n\");\n}\n\nasync function fetchJsonWithTimeout(url, timeoutMs = 1200) {\n const ctl = new AbortController();\n const tid = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const res = await fetch(url, { signal: ctl.signal });\n if (!res.ok) return null;\n return await res.json();\n } catch {\n return null;\n } finally {\n clearTimeout(tid);\n }\n}\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction getHttpPathname(req) {\n let p = String(req.url || \"\").split(\"?\")[0];\n /* Reverse proxies / clients sometimes append `/` — avoid silent 404 on e.g. `/rhizoh/genesis/stream/`. */\n while (p.length > 1 && p.endsWith(\"/\")) {\n p = p.slice(0, -1);\n }\n return p;\n}\n\nfunction rhizohLlmEnvConfigured() {\n const p = String(process.env.CASTLE_LLM_PROVIDER || \"openai\").toLowerCase();\n const key =\n p === \"anthropic\"\n ? process.env.ANTHROPIC_API_KEY\n : p === \"gemini\"\n ? process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY\n : p === \"xai\"\n ? process.env.XAI_API_KEY\n : p === \"deepseek\"\n ? process.env.DEEPSEEK_API_KEY\n : p === \"mistral\"\n ? process.env.MISTRAL_API_KEY\n : p === \"openrouter\"\n ? process.env.OPENROUTER_API_KEY\n : process.env.OPENAI_API_KEY;\n return !!String(key || \"\").trim();\n}\n\n/**\n * @param {import(\"firebase-admin/firestore\").Firestore | null} db\n * @param {number} timeoutMs\n */\nasync function probeFirestoreReachable(db, timeoutMs = 2000) {\n if (!db) return { ok: false, reason: \"no_db\" };\n const timeout = sleep(timeoutMs).then(() => ({ ok: false, reason: \"timeout\" }));\n const work = db\n .collection(\"_castle_gateway_health\")\n .doc(\"ping\")\n .get()\n .then(() => ({ ok: true, reason: \"reachable\" }))\n .catch((e) => ({ ok: false, reason: String(e?.message || e || \"firestore_error\") }));\n return Promise.race([work, timeout]);\n}\n\nfunction stripHtml(text = \"\") {\n return String(text)\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \" \")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \" \")\n .replace(/<[^>]+>/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction haversineMeters(aLat, aLon, bLat, bLon) {\n const R = 6371000;\n const dLat = ((bLat - aLat) * Math.PI) / 180;\n const dLon = ((bLon - aLon) * Math.PI) / 180;\n const sa = Math.sin(dLat / 2);\n const sb = Math.sin(dLon / 2);\n const x = sa * sa + Math.cos((aLat * Math.PI) / 180) * Math.cos((bLat * Math.PI) / 180) * sb * sb;\n return 2 * R * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));\n}\n\nasync function checkPublishTargetHealth(protocol, target) {\n const p = String(protocol || \"\").toUpperCase();\n const t = String(target || \"\").trim();\n if (!t) return { ok: false, reason: \"target_missing\", shouldReconnect: false };\n if (p === \"RTMP\") {\n const ok = t.startsWith(\"rtmp://\") || t.startsWith(\"rtmps://\");\n return { ok, reason: ok ? \"rtmp_target_format_ok\" : \"invalid_rtmp_target\", shouldReconnect: !ok, reconnectInSec: 15 };\n }\n if (p === \"WHIP\" || p === \"SFU\") {\n try {\n const res = await fetch(t, { method: \"OPTIONS\" });\n const ok = res.status < 500;\n return { ok, reason: `http_${res.status}`, shouldReconnect: !ok, reconnectInSec: 8 };\n } catch {\n return { ok: false, reason: \"network_unreachable\", shouldReconnect: true, reconnectInSec: 8 };\n }\n }\n return { ok: true, reason: \"unchecked_protocol\", shouldReconnect: false };\n}\n\nasync function fetchTextFromUrl(url) {\n const res = await fetch(url, { method: \"GET\" });\n if (!res.ok) throw new Error(`fetch_${res.status}`);\n const ct = String(res.headers.get(\"content-type\") || \"\");\n if (ct.includes(\"application/pdf\")) throw new Error(\"pdf_binary_not_supported_directly\");\n const body = await res.text();\n return stripHtml(body).slice(0, 14000);\n}\n\nfunction buildTaskActionGraph(task = \"\", context = {}) {\n const t = String(task || \"\").toLowerCase();\n if (t.includes(\"dinner\") || t.includes(\"akşam yemeği\") || t.includes(\"aksam yemegi\")) {\n return {\n taskTitle: \"Dinner Prep Planner\",\n nodes: [\n { id: \"n1\", title: \"Kitchen preheat\", action: \"smart-oven.preheat\", params: { temperatureC: 190, mode: \"fan\" } },\n { id: \"n2\", title: \"Lighting comfort\", action: \"smart-light.set_scene\", params: { scene: \"warm_dinner\" } },\n { id: \"n3\", title: \"Fridge inventory check\", action: \"smart-fridge.inventory_scan\", params: {} },\n { id: \"n4\", title: \"Reminder notification\", action: \"smart-watch.notify\", params: { text: \"Dinner prep started.\" } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" },\n { from: \"n3\", to: \"n4\" }\n ]\n };\n }\n return {\n taskTitle: task || \"General Planner\",\n nodes: [\n { id: \"n1\", title: \"Safety check\", action: \"device.safety_check\", params: { context } },\n { id: \"n2\", title: \"Primary action\", action: \"device.execute_task\", params: { task } },\n { id: \"n3\", title: \"Report\", action: \"studio.timeline_note\", params: { task } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" }\n ]\n };\n}\n\nfunction buildEthicsPrograms() {\n return {\n policyVersion: \"v1\",\n tracks: [\n {\n id: \"elderly-care-assist\",\n audience: \"elderly\",\n title: \"Elderly Assistance Track\",\n principles: [\"human-in-the-loop\", \"consent-first\", \"low-friction alerts\"],\n modules: [\"medication-reminder\", \"fall-risk-check\", \"family-notify\", \"daily-companion-brief\"]\n },\n {\n id: \"kids-safe-learning\",\n audience: \"children\",\n title: \"Kids Safe Learning Track\",\n principles: [\"guardian-consent\", \"age-appropriate-language\", \"content-filtering\"],\n modules: [\"study-coach\", \"screen-time-balance\", \"safe-social-share\", \"emergency-contact\"]\n }\n ]\n };\n}\n\nasync function deliverViaTelegram(channel, text) {\n const chatId = String(channel.endpoint || \"\").trim();\n if (!TELEGRAM_BOT_TOKEN) throw new Error(\"telegram_token_missing\");\n if (!chatId) throw new Error(\"telegram_chat_id_missing\");\n const url = `https://api.telegram.org/bot${encodeURIComponent(TELEGRAM_BOT_TOKEN)}/sendMessage`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ chat_id: chatId, text })\n });\n if (!res.ok) throw new Error(`telegram_http_${res.status}`);\n return { provider: \"telegram\", status: res.status };\n}\n\nasync function deliverViaWhatsapp(channel, text) {\n const to = String(channel.endpoint || \"\").trim();\n if (!WHATSAPP_TOKEN) throw new Error(\"whatsapp_token_missing\");\n if (!WHATSAPP_PHONE_NUMBER_ID) throw new Error(\"whatsapp_phone_number_id_missing\");\n if (!to) throw new Error(\"whatsapp_to_missing\");\n const url = `https://graph.facebook.com/v22.0/${encodeURIComponent(WHATSAPP_PHONE_NUMBER_ID)}/messages`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${WHATSAPP_TOKEN}` },\n body: JSON.stringify({ messaging_product: \"whatsapp\", to, type: \"text\", text: { body: text } })\n });\n if (!res.ok) throw new Error(`whatsapp_http_${res.status}`);\n return { provider: \"whatsapp\", status: res.status };\n}\n\nasync function deliverViaWebhook(channel, text) {\n const endpoint = String(channel.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"webhook_endpoint_missing\");\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text, platform: channel.platform || \"webhook\", ts: Date.now() })\n });\n if (!res.ok) throw new Error(`webhook_http_${res.status}`);\n return { provider: \"webhook\", status: res.status };\n}\n\nasync function deliverSocialMessage(channel, text) {\n const platform = String(channel.platform || \"webhook\").toLowerCase();\n if (platform === \"telegram\") return deliverViaTelegram(channel, text);\n if (platform === \"whatsapp\") return deliverViaWhatsapp(channel, text);\n return deliverViaWebhook(channel, text);\n}\n\nasync function deliverSocialWithRetry(channel, text) {\n let attempt = 0;\n let lastError = null;\n while (attempt <= SOCIAL_RETRY_MAX) {\n try {\n const result = await deliverSocialMessage(channel, text);\n return { ok: true, attempt: attempt + 1, ...result };\n } catch (error) {\n lastError = error;\n if (attempt >= SOCIAL_RETRY_MAX) break;\n const jitter = Math.floor(Math.random() * 90);\n await sleep(SOCIAL_RETRY_BASE_MS * Math.pow(2, attempt) + jitter);\n }\n attempt += 1;\n }\n return { ok: false, attempt: attempt + 1, error: String(lastError?.message || \"delivery_failed\") };\n}\n\nasync function dispatchCommandToAdapter(device, command) {\n const adapter = String(command?.adapter || device?.adapter || \"websocket\").toLowerCase();\n const endpoint = String(device?.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"device_endpoint_missing\");\n if (adapter === \"mqtt\") {\n // MQTT delivery is proxied via a bridge endpoint (HTTP webhook style).\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n type: \"mqtt-command\",\n topic: `castle/devices/${device.id}/commands`,\n action: command.action,\n params: command.params || {}\n })\n });\n if (!res.ok) throw new Error(`mqtt_bridge_http_${res.status}`);\n return { ok: true, adapter, via: \"http-bridge\", status: res.status };\n }\n if (!endpoint.startsWith(\"ws://\") && !endpoint.startsWith(\"wss://\")) throw new Error(\"invalid_websocket_endpoint\");\n const sendResult = await new Promise((resolve, reject) => {\n const ws = new WebSocket(endpoint);\n const timer = setTimeout(() => {\n try {\n ws.close();\n } catch {\n /* noop */\n }\n reject(new Error(\"websocket_timeout\"));\n }, 3000);\n ws.on(\"open\", () => {\n ws.send(\n JSON.stringify({\n type: \"castle-command\",\n commandId: command.id,\n deviceId: command.deviceId,\n action: command.action,\n params: command.params || {}\n }),\n (err) => {\n clearTimeout(timer);\n ws.close();\n if (err) reject(new Error(\"websocket_send_failed\"));\n else resolve({ ok: true, adapter: \"websocket\", via: \"ws\" });\n }\n );\n });\n ws.on(\"error\", () => {\n clearTimeout(timer);\n reject(new Error(\"websocket_connect_failed\"));\n });\n });\n return sendResult;\n}\n\nfunction readHttpJson(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => {\n if (!buf) return resolve({});\n const payload = safeJsonParse(buf);\n if (!payload || typeof payload !== \"object\") return reject(new Error(\"invalid_json_payload\"));\n resolve(payload);\n });\n req.on(\"error\", reject);\n });\n}\n\n/** Ham gövde (HMAC doğrulaması için). */\nfunction readHttpBodyText(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => resolve(buf));\n req.on(\"error\", reject);\n });\n}\n\nfunction maskOpaqueId(value) {\n const t = String(value || \"\");\n if (!t) return \"(empty)\";\n if (t.length <= 8) return \"****\";\n return `${t.slice(0, 4)}…${t.slice(-2)}`;\n}\n\nfunction readGatewayHttpToken(req) {\n const x = String(req.headers?.[\"x-castle-gateway-token\"] || \"\").trim();\n if (x) return x;\n const auth = String(req.headers?.authorization || \"\");\n const m = /^Bearer\\s+(.+)$/i.exec(auth);\n return m ? String(m[1] || \"\").trim() : \"\";\n}\n\nasync function resolveHttpUser(req) {\n const authResult = await verifyClientToken(req);\n if (authResult?.ok && authResult.user?.uid) {\n return { ok: true, uid: String(authResult.user.uid), mode: authResult.kind || \"auth\" };\n }\n if (ALLOW_DEV_ANON && ALLOW_DEV_HTTP_UID) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) return { ok: true, uid: `dev-${devUid.slice(0, 64)}`, mode: \"dev-header\" };\n }\n return { ok: false, reason: authResult?.reason || \"auth_required\" };\n}\n\n/**\n * Epistemic ledger ingest — Firebase uid OR valid transport token + X-Castle-Dev-Uid.\n * Prod disables CASTLE_ALLOW_DEV_HTTP_UID; LLM may still work while telemetry 401'd without this.\n */\nasync function resolveEpistemicIngestActor(req) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok && tok === REQUIRED_GATEWAY_TOKEN) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) {\n return { ok: true, uid: `gw-telemetry-${devUid.slice(0, 64)}`, mode: \"gateway-token-dev-uid\" };\n }\n return { ok: false, reason: \"dev_uid_required\" };\n }\n if (tok) {\n return { ok: false, reason: \"gateway_token_mismatch\" };\n }\n }\n const auth = await resolveHttpUser(req);\n if (auth.ok) return auth;\n return { ok: false, reason: auth.reason || \"auth_required\" };\n}\n\n/** Mesh actor: Firebase uid, dev header, or `guest:<uuid>` via header / `guestId` query (SSE cannot set headers). */\nasync function resolveMeshActor(req) {\n const auth = await resolveHttpUser(req);\n if (auth.ok) return { ok: true, clientUid: auth.uid, mode: auth.mode };\n if (ALLOW_MESH_GUEST) {\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const gid = String(req.headers?.[\"x-castle-guest-id\"] || q.searchParams.get(\"guestId\") || \"\").trim();\n if (gid) {\n const normalized = gid.startsWith(\"guest:\") ? gid.slice(0, 180) : `guest:${gid.slice(0, 160)}`;\n return { ok: true, clientUid: normalized, mode: \"guest-header\" };\n }\n }\n return { ok: false, reason: \"mesh_auth_required\" };\n}\n\n/** Live genesis runtime JSON (GET /rhizoh/genesis/runtime + SSE infra sampler). */\nasync function buildGenesisRuntimeSurfacePayloadLive() {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n let spiralWsActive = 0;\n if (spiralWssForGenesis) {\n for (const c of spiralWssForGenesis.clients) {\n if (c.readyState === 1) spiralWsActive += 1;\n }\n }\n const base = buildGenesisRuntimeSurfacePayload({\n infraMetrics,\n scoredHealth: scored,\n rhizohEnterpriseMetrics,\n mesh: meshContinuityAggregate(),\n workerHealth: worker,\n port: PORT,\n spiralWebSocketClientsActive: spiralWsActive\n });\n return {\n ...base,\n replayFingerprint: computeGenesisReplayFingerprintV0(base),\n gatewayCapabilities: buildGenesisGatewayCapabilitiesV0(),\n genesisStream: {\n schema: \"castle.genesis.stream_cursor.v0\",\n lastAcceptedSeq: getGenesisContinuitySeq(),\n clientIngress: {\n schema: GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n contract: \"castle.world_observation.ingress_envelope.v1\",\n postPath: rhizohRuntime.routes.genesisIngress,\n closure: getGenesisWorldObservationIngressStatusV0()\n },\n seqContinuity: getGenesisContinuitySeqAuditV0(),\n eventArchive: genesisContinuityEventArchiveEnabled()\n ? {\n enabled: true,\n projection: GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION,\n queryPath: rhizohRuntime.routes.genesisContinuityEvents\n }\n : { enabled: false },\n replayRouter: {\n schema: GENESIS_REPLAY_ROUTER_SCHEMA,\n queryPath: rhizohRuntime.routes.genesisReplay,\n queryHint: \"from=&to= or range=from-to; optional type=&checkpoints=0\",\n temporalDiffPath: rhizohRuntime.routes.genesisReplayDiff,\n temporalDiffSchema: GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA,\n equivalencePath: rhizohRuntime.routes.genesisReplayEquivalence,\n analyticsPath: rhizohRuntime.routes.genesisReplayAnalytics,\n evolutionPath: rhizohRuntime.routes.genesisReplayEvolution\n }\n }\n };\n}\n\nconst httpServer = createServer(async (req, res) => {\n const corsAllow = applyHttpCorsHeaders(req, res);\n if (req.method === \"OPTIONS\") {\n if (!corsAllow && req.headers?.origin) {\n console.warn(\"[GATEWAY_CORS] preflight rejected for origin:\", req.headers.origin);\n }\n res.writeHead(corsAllow ? 204 : 403);\n res.end();\n return;\n }\n\n const pathname = getHttpPathname(req);\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/genesis/__ping\") {\n sendJson(res, 200, {\n ok: true,\n pid: process.pid,\n entry: __filename,\n time: Date.now(),\n genesis: true\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/epistemic/__probe\") {\n sendJson(res, 200, {\n ok: true,\n epistemicIngestActorV2: true,\n routes: {\n seal: rhizohRuntime.routes.epistemicSeal,\n logsBatch: rhizohRuntime.routes.epistemicLogsBatch\n },\n hasGatewayToken: Boolean(REQUIRED_GATEWAY_TOKEN),\n gatewayTokenLen: REQUIRED_GATEWAY_TOKEN ? REQUIRED_GATEWAY_TOKEN.length : 0\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/webhooks/stripe\") {\n await handleStripeMembershipWebhook(req, res);\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/billing/stripe/checkout\") {\n await handleStripeCheckoutCreate(req, res, { readHttpJson, resolveHttpUser, sendJson });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/live\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, live: false, dns: true, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n sendJson(res, 200, { ok: true, live: true, dns: true, service: \"castle-gateway\", ts: Date.now() });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/cors\") {\n const reqOrigin = normalizeHttpOrigin(req.headers?.origin);\n const allowOrigin = accessControlAllowOriginValue(req);\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n requestOrigin: reqOrigin || null,\n allowed: Boolean(allowOrigin),\n allowOrigin: allowOrigin || null,\n allowSetSize: httpCors.allowSet.size\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/ready\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, ready: false, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n try {\n const persistence = getFirebasePersistence();\n const llmProbe = rhizohLlmEnvConfigured();\n sendJson(res, 200, {\n ok: true,\n ready: true,\n dns: true,\n persistence: persistence.mode,\n service: \"castle-gateway\",\n gateway: \"ok\",\n llm: llmProbe ? \"ok\" : \"degraded\",\n spine: llmProbe ? \"ok\" : \"partial\"\n });\n } catch (e) {\n sendJson(res, 503, { ok: false, ready: false, reason: String(e?.message || e), service: \"castle-gateway\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/reality\") {\n const payload = buildRealityHealthPayload();\n sendJson(res, payload.ok ? 200 : 503, payload);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/substrate\") {\n const authority = buildGatewaySubstrateAuthoritySnapshotV0();\n const operational = buildSubstrateOperationalSnapshotV0();\n const drift = buildRealityHealthPayload();\n const ok = Boolean(authority.productionAuthoritySatisfied) && drift.ok;\n sendJson(res, ok ? 200 : 503, {\n ok,\n service: \"castle-gateway\",\n ts: Date.now(),\n authority,\n operational,\n drift: { ok: drift.ok, errors: drift.drift?.errors, warnings: drift.drift?.warnings }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/substrate/health\") {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const body = await readHttpJson(req);\n const uid = (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestClientSubstrateHealthV0(body, { uid, clientId: uid });\n if (!result.ok) {\n sendJson(res, 400, result);\n return;\n }\n recordSubstrateOtelEventV0(\"client_health_ingest\", { uid: String(uid).slice(0, 32) });\n sendJson(res, 200, result);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/deps\") {\n const t0 = Date.now();\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, {\n ok: false,\n dns: true,\n llm: false,\n firestore: false,\n phase: \"maintenance\",\n latencyMs: Date.now() - t0,\n service: \"castle-gateway\"\n });\n return;\n }\n const persistence = getFirebasePersistence();\n let firestoreOk = persistence.mode !== \"firebase\";\n let firestoreDetail = persistence.mode === \"file\" ? \"file_mode\" : \"not_applicable\";\n if (persistence.mode === \"firebase\" && persistence.db) {\n const pr = await probeFirestoreReachable(persistence.db, 2000);\n firestoreOk = pr.ok;\n firestoreDetail = pr.reason || (pr.ok ? \"reachable\" : \"fail\");\n } else if (persistence.mode === \"firebase\" && !persistence.db) {\n firestoreOk = false;\n firestoreDetail = \"firebase_not_initialized\";\n }\n const llmOk = rhizohLlmEnvConfigured();\n const overallOk = llmOk && firestoreOk;\n const latencyMs = Date.now() - t0;\n sendJson(res, 200, {\n ok: overallOk,\n dns: true,\n llm: llmOk,\n firestore: persistence.mode === \"firebase\" ? firestoreOk : false,\n persistence: persistence.mode,\n firestoreDetail,\n latencyMs,\n service: \"castle-gateway\",\n wsPort: PORT\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health\") {\n const persistence = getFirebasePersistence();\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n wsPort: PORT,\n persistence: persistence.mode,\n presenceMesh: \"sse\"\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/health\") {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n sendJson(res, 200, {\n ok: true,\n role: \"gateway\",\n queueLagMs: infraMetrics.queueLag,\n queueDepth: infraMetrics.queueDepth,\n errors: infraMetrics.errors,\n status: scored.status,\n score: scored.score,\n reasons: scored.reasons,\n worker: worker\n ? {\n status: worker.status,\n score: worker.score,\n reasons: worker.reasons\n }\n : null,\n timestamp: Date.now()\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisRuntime) {\n const payload = await buildGenesisRuntimeSurfacePayloadLive();\n sendJson(res, 200, payload);\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.genesisIngress) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const actor = await resolveMeshActor(req);\n const clientId = actor.ok ? actor.clientUid : (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestGenesisWorldObservationV0(body, { clientId });\n if (!result.ok) {\n const status =\n result.error === \"rate_limited\" || result.error === \"agent_spoke_throttled\"\n ? 429\n : result.error === \"payload_too_large\"\n ? 413\n : 400;\n sendJson(res, status, result);\n return;\n }\n sendJson(res, result.idempotent ? 200 : 202, result);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisStream) {\n const corsOrigin = accessControlAllowOriginValue(req);\n if (!corsOrigin && String(process.env.NODE_ENV || \"\").toLowerCase() === \"production\") {\n const og = String(req.headers?.origin || \"\").trim();\n if (og) {\n console.warn(\"[GATEWAY] genesis SSE: missing Access-Control-Allow-Origin for browser Origin (CORS whitelist mismatch):\", og);\n }\n }\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n res.write(\"retry: 3000\\n\\n\");\n res.write(\": genesis continuity stream\\n\\n\");\n res.write(\"event: genesis\\n\");\n res.write(`data: {\"booted\":true,\"pid\":${process.pid}}\\n\\n`);\n registerGenesisSseClient(res, req);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLatest) {\n const cp = getLatestGenesisSignedCheckpoint();\n if (!cp) {\n sendJson(res, 404, { ok: false, error: \"no_checkpoint_yet\", hint: \"wait_for_seq_interval_or_configure_signing_secret\" });\n return;\n }\n sendJson(res, 200, { ok: true, checkpoint: cp });\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(rhizohRuntime.routes.genesisCheckpointBySeqPrefix)) {\n const tail = pathname.slice(rhizohRuntime.routes.genesisCheckpointBySeqPrefix.length).split(\"/\")[0];\n const seq = parseInt(String(tail || \"\").trim(), 10);\n const out = await genesisCheckpointQueryBySeqV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"checkpoint_not_found\") {\n sendJson(res, 404, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointRange) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryRangeV0(from, to);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"range_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLineage) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const seq = parseInt(String(u.searchParams.get(\"seq\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryLineageV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"lineage_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisContinuityEvents) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const limit = parseInt(String(u.searchParams.get(\"limit\") || \"\").trim(), 10);\n const out = await queryGenesisContinuityEventArchiveV0(from, to, type, limit);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\" || out.error === \"event_archive_disabled\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"archive_file_too_large\") {\n sendJson(res, 413, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplay) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await resolveGenesisReplayRouterV1({\n from: fromSeq,\n to: toSeq,\n type,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayDiff) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const out = await computeGenesisReplayTemporalDiffV1({ from: fromSeq, to: toSeq, type });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEquivalence) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from1 = parseInt(String(u.searchParams.get(\"from1\") || \"\").trim(), 10);\n const to1 = parseInt(String(u.searchParams.get(\"to1\") || \"\").trim(), 10);\n const from2 = parseInt(String(u.searchParams.get(\"from2\") || \"\").trim(), 10);\n const to2 = parseInt(String(u.searchParams.get(\"to2\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await compareGenesisReplayEquivalenceV1({\n from1,\n to1,\n from2,\n to2,\n type,\n includeCheckpoints\n });\n if (\n !out.ok &&\n (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")\n ) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayAnalytics) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayAnalyticsV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEvolution) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const collapseWindows = parseInt(String(u.searchParams.get(\"collapseWindows\") || \"0\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayEvolutionV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n collapseWindows,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/metrics\") {\n res.writeHead(200, { \"Content-Type\": \"text/plain; version=0.0.4; charset=utf-8\" });\n res.end(renderInfraMetricsProm());\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.infraPrometheusMetrics) {\n if (process.env.CASTLE_PROMETHEUS_METRICS !== \"1\") {\n sendJson(res, 404, { ok: false, error: \"prometheus_metrics_disabled\" });\n return;\n }\n res.writeHead(200, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(renderRhizohPrometheusMetrics());\n return;\n }\n if (req.method === \"GET\" && pathname.startsWith(\"/infra/traces/\")) {\n const traceId = decodeURIComponent(pathname.slice(\"/infra/traces/\".length));\n const item = await getTraceById(traceId);\n if (!item) return sendJson(res, 404, { ok: false, error: \"trace_not_found\" });\n return sendJson(res, 200, { ok: true, trace: item });\n }\n if (req.method === \"GET\" && pathname === \"/infra/traces\") {\n const u = new URL(String(req.url || \"/infra/traces\"), `http://${req.headers.host || \"localhost\"}`);\n const sessionId = String(u.searchParams.get(\"sessionId\") || \"\").trim();\n const items = sessionId ? await listTracesBySession(sessionId) : [];\n return sendJson(res, 200, { ok: true, sessionId, traces: items });\n }\n\n /** Presence room mesh (C): JOIN / LEAVE / SNAPSHOT / DELTA / REPLAY (HTTP); SUBSCRIBE = SSE stream. */\n if (req.method === \"POST\" && pathname === \"/presence/mesh/join\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid, clientUid: actor.clientUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/leave\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshLeave(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/snapshot\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n sendJson(res, 200, meshSnapshot(roomUid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/delta\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, MAX_MESSAGE_BYTES);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const seq = meshAppendDelta(roomUid, actor.clientUid, {\n node: body?.node,\n projectionPatch: body?.projectionPatch,\n writerSubject: body?.writerSubject\n });\n sendJson(res, 200, { ok: true, roomUid, seq });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/replay\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const fromNodeId = body?.fromNodeId != null ? String(body.fromNodeId) : \"\";\n const fromSeq = body?.fromSeq;\n if (fromNodeId) {\n sendJson(res, 200, meshReplayFromNodeId(roomUid, fromNodeId));\n return;\n }\n sendJson(res, 200, meshReplayFromSeq(roomUid, fromSeq));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/presence/mesh/subscribe\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const roomUid = String(q.searchParams.get(\"roomUid\") || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n const write = (chunk) => {\n try {\n res.write(chunk);\n } catch {\n /* closed */\n }\n };\n const unsub = meshSubscribeSse(roomUid, write);\n const onClose = () => {\n unsub();\n meshLeave(roomUid, actor.clientUid);\n try {\n res.end();\n } catch {\n /* ignore */\n }\n };\n req.on(\"close\", onClose);\n req.on(\"aborted\", onClose);\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, uid: auth.uid, mode: auth.mode, items: await listConnections(auth.uid) });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const id = await createConnection(auth.uid, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"create_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n await updateConnection(auth.uid, id, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"update_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"DELETE\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n await deleteConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"delete_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections/test\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n if (!checkHttpRateLimit(`llm_conn_test:${auth.uid}`, RL_LLM_CONN_TEST_PER_MIN, 60_000)) {\n return sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n }\n const hasBodyKey = !!String(payload?.apiKey || \"\").trim();\n const result = await queryRhizohLlm(\n {\n message: String(payload?.message || \"Ping from Castle Gateway\"),\n context: payload?.context || { source: \"connection_test\" },\n provider: payload?.provider,\n model: payload?.model,\n apiKey: payload?.apiKey || \"\"\n },\n { allowExternalApiKey: true, llmKeySource: hasBodyKey ? \"auto\" : \"env\" }\n );\n logLlmAccess({\n route: \"/llm/connections/test\",\n uid: auth.uid,\n llmKeyBillingOwner: result.llmKeyBillingOwner,\n llmKeyOrigin: result.llmKeyOrigin,\n provider: result.provider,\n model: result.model,\n connectionId: null\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"connection_test_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAgentIdentities(auth.uid);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerAgentIdentity({\n ownerUid: auth.uid,\n agentId: payload?.agentId,\n role: payload?.role,\n personaSeed: payload?.personaSeed || {},\n capabilityLevel: payload?.capabilityLevel\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/agents/identities/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const owns = await getAgentIdentity(id);\n if (!owns || owns.ownerUid !== auth.uid) return sendJson(res, 403, { ok: false, error: \"forbidden\" });\n const payload = await readHttpJson(req);\n const row = await updateAgentIdentity(id, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/academy/events\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/academy/events\"}`);\n const status = String(url.searchParams.get(\"status\") || \"\");\n const items = await listAcademyEvents({ ownerUid: auth.uid, status, limit: 120 });\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/academy/events\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const ev = await queueAcademyEvent({\n type: payload?.type,\n roomId: payload?.roomId,\n ownerUid: auth.uid,\n participants: payload?.participants || [],\n topic: payload?.topic || \"academy\",\n payload: payload?.payload || {}\n });\n sendJson(res, 200, { ok: true, event: ev });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && String(req.url || \"\").startsWith(\"/academy/events/\") && String(req.url || \"\").endsWith(\"/resolve\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const parts = String(req.url || \"\").split(\"/\").filter(Boolean);\n const eventId = parts[2] || \"\";\n try {\n const payload = await readHttpJson(req);\n const out = await resolveAcademyEvent(eventId, payload || {});\n sendJson(res, 200, { ok: true, event: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_resolve_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/memory\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/memory\"}`);\n const scope = url.searchParams.get(\"scope\") === \"agents\" ? \"agents\" : \"users\";\n const id = String(url.searchParams.get(\"id\") || (scope === \"agents\" ? \"\" : auth.uid));\n const kind = String(url.searchParams.get(\"kind\") || \"\");\n const limit = Number(url.searchParams.get(\"limit\") || 40);\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n sendJson(res, 200, { ok: true, scope, id, kind, items: await listMemories({ scope, id, kind, limit }) });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const profile = await getPersonaGoalMemory(auth.uid);\n sendJson(res, 200, { ok: true, profile });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const profile = await setPersonaGoalMemory({\n uid: auth.uid,\n personaPatch: payload?.personaPatch || {},\n goals: Array.isArray(payload?.goals) ? payload.goals : null,\n preferencesPatch: payload?.preferencesPatch || {}\n });\n sendJson(res, 200, { ok: true, profile });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"profile_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/compact\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const out = await autoCompactMemories({ scope, id });\n sendJson(res, 200, { ok: true, scope, id, ...out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_compact_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/context\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const context = await getMemoryContext({\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n query: String(payload?.query || \"\"),\n limit: Number(payload?.limit || 80)\n });\n sendJson(res, 200, { ok: true, context });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_context_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/place-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const placeName = String(payload?.placeName || \"\").trim();\n if (!placeName) return sendJson(res, 400, { ok: false, error: \"placeName_required\" });\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const persona = String(payload?.waypointPersona || payload?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(placeName)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Place: ${placeName}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Give a concise conversational briefing about this place for a walking-route companion.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-place-brief\", placeName, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `PLACE_BRIEF:${placeName} => ${String(summary.reply || \"\").slice(0, 900)}`,\n tags: [\"place\", \"brief\", \"event-layer\"],\n importance: 0.62,\n meta: { placeName }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"place-brief\",\n text: `${placeName} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: {\n placeName,\n directive: summary.directive,\n lat: hit?.lat ? Number(hit.lat) : null,\n lon: hit?.lon ? Number(hit.lon) : null\n }\n });\n sendJson(res, 200, { ok: true, place: hit || null, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"place_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/route-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const waypoints = Array.isArray(payload?.waypoints) ? payload.waypoints.slice(0, 24) : [];\n const out = [];\n for (const wp of waypoints) {\n const name = String(wp?.name || \"\").trim();\n if (!name) continue;\n const persona = String(wp?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(name)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Waypoint: ${name}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Generate a concise spoken briefing for route waypoint.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-route-brief\", waypoint: name, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n out.push({\n name,\n persona,\n place: hit || null,\n reply: summary.reply,\n directive: summary.directive\n });\n }\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `ROUTE_BRIEF: waypoints=${out.length} ${out.map((x) => `${x.name}:${x.persona}`).join(\" | \")}`.slice(0, 1300),\n tags: [\"route\", \"waypoint\", \"brief\", \"event-layer\"],\n importance: 0.74,\n meta: { count: out.length }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"route-brief\",\n text: out.map((x) => `${x.name}:${String(x.reply || \"\").slice(0, 140)}`).join(\" | \").slice(0, 1400),\n roomId: \"studio-main\",\n meta: {\n waypointCount: out.length,\n waypoints: out.map((x) => ({\n name: x.name,\n persona: x.persona,\n lat: x.place?.lat ? Number(x.place.lat) : null,\n lon: x.place?.lon ? Number(x.place.lon) : null\n }))\n }\n });\n sendJson(res, 200, { ok: true, items: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"route_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/pdf-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const title = String(payload?.title || \"document\");\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const providedText = String(payload?.text || \"\");\n const url = String(payload?.url || \"\");\n let content = providedText;\n if (!content && url) content = await fetchTextFromUrl(url);\n if (!content) return sendJson(res, 400, { ok: false, error: \"text_or_supported_url_required\" });\n const summary = await queryRhizohLlm(\n {\n message: `Summarize this document and provide key points + practical actions.\\nTitle:${title}\\nContent:\\n${content.slice(0, 12000)}`,\n context: { source: \"event-layer-pdf-brief\", title },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `DOC_BRIEF:${title} => ${String(summary.reply || \"\").slice(0, 1000)}`,\n tags: [\"document\", \"brief\", \"library\", \"event-layer\"],\n importance: 0.7,\n meta: { title, url: url || null }\n });\n await addTranscript(auth.uid, {\n source: \"library-brief\",\n eventType: \"pdf-brief\",\n text: `${title} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { title }\n });\n sendJson(res, 200, { ok: true, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/pdf/upload\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req, 8 * 1024 * 1024);\n const job = await enqueuePdfJob({\n uid: auth.uid,\n fileName: payload?.fileName || \"document.pdf\",\n bytesBase64: payload?.contentBase64\n });\n sendJson(res, 200, { ok: true, job });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_upload_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/session\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createPublishSession(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session\",\n text: `Publish session armed: ${row.protocol} ${row.target}`.slice(0, 500),\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: row.id, bridge: row.bridge }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/studio/publish/sessions\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listPublishSessions(auth.uid, 80);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/health-check\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const protocol = String(payload?.protocol || \"WHIP\");\n const target = String(payload?.target || \"\");\n const health = await checkPublishTargetHealth(protocol, target);\n if (payload?.sessionId) {\n await updatePublishSession(auth.uid, String(payload.sessionId), {\n status: health.ok ? \"healthy\" : \"degraded\",\n reconnectPolicy: {\n enabled: true,\n backoffSec: [2, 5, 10, 20],\n nextRetryInSec: health.reconnectInSec || 0,\n maxRetries: 8,\n reason: health.reason\n }\n });\n }\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-health\",\n text: `${protocol} ${target} => ${health.ok ? \"healthy\" : \"degraded\"} (${health.reason})`,\n roomId: \"studio-main\",\n meta: { protocol, target, reconnect: !!health.shouldReconnect }\n });\n sendJson(res, 200, { ok: true, health });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_health_check_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/studio/publish/sessions/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updatePublishSession(auth.uid, id, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session-update\",\n text: `Session ${id} -> ${row.status || \"updated\"}`,\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: id, status: row.status, ingestUrl: row.ingestUrl || \"\" }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/transcripts\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/transcripts\"}`);\n const limit = Math.max(1, Math.min(200, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await listTranscripts(auth.uid, limit);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/transcripts\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await addTranscript(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"transcript_write_failed\" });\n }\n return;\n }\n\n /** E1: broadcast intent → GreenRoom capability → transcript + memory artifact */\n if (req.method === \"POST\" && req.url === \"/studio/capabilities/greenroom\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const traceId = String(payload?.traceId || `TRC-${Date.now().toString(36).toUpperCase()}`);\n const title = String(payload?.title || \"Castle Live Broadcast\").slice(0, 200);\n const intentRaw = String(payload?.intentRaw || \"\").slice(0, 800);\n const audienceEstimate = Math.max(0, Math.min(50_000_000, Number(payload?.audienceEstimate) || 1200));\n const roomId = String(payload?.roomId || \"greenroom-main\");\n const transcriptRow = await addTranscript(auth.uid, {\n source: \"rhizoh\",\n eventType: \"BROADCAST_ROUTED\",\n text: `GreenRoom · ${title}${intentRaw ? `\\nIntent: ${intentRaw}` : \"\"}`,\n roomId,\n meta: {\n ack: \"BROADCAST_ROUTED\",\n capability: \"greenroom\",\n intentType: \"broadcast\",\n traceId,\n audienceEstimate,\n artifact: {\n kind: \"broadcast_card\",\n title,\n traceId,\n roomId,\n createdAt: Date.now()\n }\n }\n });\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n text: `GREENROOM_BROADCAST:${title} trace=${traceId}`,\n tags: [\"broadcast\", \"greenroom\", \"artifact\", \"castle-library\"],\n importance: 0.72,\n kind: \"episodic\",\n meta: { traceId, capability: \"greenroom\", transcriptId: transcriptRow.id }\n });\n sendJson(res, 200, {\n ok: true,\n ack: \"BROADCAST_ROUTED\",\n traceId,\n transcript: transcriptRow,\n replayPath: `/replay/${encodeURIComponent(traceId)}`,\n sharePath: `/replay/${encodeURIComponent(traceId)}`\n });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"greenroom_capability_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/metadata/search\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/metadata/search\"}`);\n const q = String(url.searchParams.get(\"q\") || \"\");\n const limit = Math.max(1, Math.min(300, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await searchMetadata(auth.uid, q, limit);\n sendJson(res, 200, { ok: true, q, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerDevice(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, device: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"device_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const lat = Number(payload?.lat);\n const lon = Number(payload?.lon);\n const speed = Math.max(0, Number(payload?.speed || 0));\n const cfgCenterLat = Number(process.env.CASTLE_GEOFENCE_CENTER_LAT || 41.0082);\n const cfgCenterLon = Number(process.env.CASTLE_GEOFENCE_CENTER_LON || 28.9784);\n const cfgRadiusM = Math.max(100, Number(process.env.CASTLE_GEOFENCE_RADIUS_M || 55000));\n const cfgSpeedCap = Math.max(1, Number(process.env.CASTLE_SPEED_CAP_MPS || 35));\n const dist = Number.isFinite(lat) && Number.isFinite(lon) ? haversineMeters(cfgCenterLat, cfgCenterLon, lat, lon) : 0;\n const geofenceOk = !Number.isFinite(dist) ? true : dist <= cfgRadiusM;\n const speedOk = speed <= cfgSpeedCap;\n const failsafe = !(geofenceOk && speedOk);\n const safety = {\n geofenceOk,\n speedOk,\n failsafe,\n distanceFromCenterM: Number.isFinite(dist) ? dist : null,\n speedCapMps: cfgSpeedCap\n };\n const row = await appendTelemetry(auth.uid, deviceId, {\n lat,\n lon,\n speed,\n battery: payload?.battery,\n mode: payload?.mode || (failsafe ? \"FAILSAFE_HOLD\" : \"ACTIVE\"),\n raw: payload?.raw || {}\n });\n await updateDevice(auth.uid, deviceId, {\n status: failsafe ? \"failsafe\" : \"active\",\n lastTelemetryAt: row.ts,\n safetyState: safety\n });\n if (failsafe) {\n await addTranscript(auth.uid, {\n source: \"robotics-guardrails\",\n eventType: \"failsafe\",\n text: `Device ${deviceId} failsafe triggered geofenceOk=${geofenceOk} speedOk=${speedOk}`,\n roomId: \"studio-main\",\n meta: { deviceId, safety }\n });\n }\n sendJson(res, 200, { ok: true, telemetry: row, safety, command: failsafe ? \"FAILSAFE_HOLD\" : \"CONTINUE\" });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"telemetry_ingest_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listTelemetry(auth.uid, 150);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createAutomation(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"automation-scheduled\",\n text: `Automation scheduled: ${row.title} at ${new Date(row.scheduleAt).toLocaleString()}`,\n roomId: \"studio-main\",\n meta: { automationId: row.id, action: row.action, deviceId: row.deviceId }\n });\n sendJson(res, 200, { ok: true, automation: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"automation_create_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAutomations(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listDevices(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const devs = await listDevices(auth.uid, 500);\n const d = devs.find((x) => x.id === deviceId);\n if (!d) return sendJson(res, 404, { ok: false, error: \"device_not_found\" });\n const action = String(payload?.action || \"\");\n const caps = Array.isArray(d.capabilityProfile) ? d.capabilityProfile : [];\n if (action && caps.length && !caps.includes(action) && !caps.includes(\"*\")) {\n return sendJson(res, 400, { ok: false, error: \"unsupported_action_for_device\", capabilityProfile: caps });\n }\n const cmd = await enqueueCommand(auth.uid, {\n deviceId,\n action: action || \"NOOP\",\n params: payload?.params || {},\n adapter: payload?.adapter || d.adapter || \"websocket\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-command-queue\",\n eventType: \"command-queued\",\n text: `${cmd.deviceId} -> ${cmd.action}`,\n roomId: \"studio-main\",\n meta: { commandId: cmd.id, adapter: cmd.adapter }\n });\n sendJson(res, 200, { ok: true, command: cmd });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listCommands(auth.uid, 200);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/robotics/commands/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updateCommandStatus(auth.uid, id, payload?.status || \"sent\");\n sendJson(res, 200, { ok: true, command: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/rhizoh/bridge\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const result = await runRhizohBrainV2({\n message: String(payload?.message || \"robotics bridge command\"),\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n worldState: payload?.worldState || {},\n deviceState: payload?.deviceState || {},\n intent: \"robotics-mechanics\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-bridge\",\n eventType: \"robotics-command\",\n text: `Bridge -> ${String(result.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { directive: result.directive, agentId: payload?.agentId || \"\" }\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"robotics_bridge_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/planner/graph\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const graph = buildTaskActionGraph(String(payload?.task || \"\"), payload?.context || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"planner-graph\",\n text: `Planner graph built: ${graph.taskTitle} nodes=${graph.nodes.length}`,\n roomId: \"studio-main\",\n meta: { taskTitle: graph.taskTitle, nodeCount: graph.nodes.length }\n });\n sendJson(res, 200, { ok: true, graph });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"planner_graph_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerSocialChannel(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, channel: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_channel_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listSocialChannels(auth.uid, 100);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/broadcast\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const text = String(payload?.text || \"\").slice(0, 1200);\n const channels = await listSocialChannels(auth.uid, 100);\n const enabled = channels.filter((c) => c.status === \"enabled\");\n const deliveries = [];\n for (const c of enabled) {\n const out = await deliverSocialWithRetry(c, text);\n deliveries.push({ id: c.id, platform: c.platform, ...out });\n }\n const okCount = deliveries.filter((d) => d.ok).length;\n await addTranscript(auth.uid, {\n source: \"social-layer\",\n eventType: \"social-broadcast\",\n text: `Broadcast delivered ${okCount}/${enabled.length} channel(s): ${text.slice(0, 240)}`,\n roomId: \"studio-main\",\n meta: { deliveries }\n });\n sendJson(res, 200, { ok: true, sentChannels: okCount, totalChannels: enabled.length, deliveries });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_broadcast_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/ethics/programs\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, ...buildEthicsPrograms() });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands/dispatch\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const all = await listCommands(auth.uid, 200);\n const queued = all.filter((c) => String(c.status || \"queued\") === \"queued\").slice(0, 50);\n const devices = await listDevices(auth.uid, 500);\n const byId = new Map(devices.map((d) => [d.id, d]));\n const results = [];\n for (const cmd of queued) {\n const dev = byId.get(cmd.deviceId);\n if (!dev) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: \"device_not_found\" });\n continue;\n }\n try {\n const dispatched = await dispatchCommandToAdapter(dev, cmd);\n await updateCommandStatus(auth.uid, cmd.id, \"sent\");\n results.push({ id: cmd.id, ok: true, adapter: dispatched.adapter, via: dispatched.via });\n } catch (error) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: String(error?.message || \"dispatch_failed\") });\n }\n }\n await addTranscript(auth.uid, {\n source: \"robotics-command-dispatch\",\n eventType: \"command-dispatch-batch\",\n text: `Dispatch batch size=${results.length} ok=${results.filter((x) => x.ok).length}`,\n roomId: \"studio-main\",\n meta: { results: results.slice(0, 30) }\n });\n sendJson(res, 200, { ok: true, processed: results.length, results });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"dispatch_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/pdf/jobs/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n const job = await getPdfJob(auth.uid, id);\n if (!job) return sendJson(res, 404, { ok: false, error: \"pdf_job_not_found\" });\n sendJson(res, 200, { ok: true, job });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const row = await appendMemory({\n scope,\n id,\n text: payload?.text,\n tags: payload?.tags || [],\n importance: payload?.importance,\n kind: payload?.kind || \"episodic\",\n meta: { uid: auth.uid, ...(payload?.meta || {}) }\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.externalTruth) {\n try {\n if (process.env.RHIZOH_EXTERNAL_TRUTH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_truth:${ip}`, RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n sendJson(res, 200, buildRhizohExternalGroundTruthPayload(req));\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.productOutcome) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const rawBody = await readHttpBodyText(req, 64 * 1024);\n const body = rawBody.trim() ? safeJsonParse(rawBody) : null;\n if (!body || typeof body !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"invalid_json_payload\" });\n return;\n }\n const sigHdr = String(req.headers?.[\"x-rhizoh-outcome-signature\"] || \"\").trim();\n const hmacV = verifyRhizohOutcomeHmac(rawBody, sigHdr);\n if (!hmacV.ok) {\n sendJson(res, 401, { ok: false, error: hmacV.error || \"outcome_hmac_invalid\" });\n return;\n }\n const srcTokHdr = String(req.headers?.[\"x-rhizoh-outcome-source-token\"] || \"\").trim();\n const srcV = verifyRhizohOutcomeSourceToken(body.source, srcTokHdr);\n if (!srcV.ok) {\n sendJson(res, 401, { ok: false, error: srcV.error || \"outcome_source_token_invalid\" });\n return;\n }\n const subjectKey = String(body.subjectKey || \"\").trim().slice(0, 128);\n if (\n subjectKey &&\n !checkHttpRateLimit(\n `rhizoh_product_outcome_subj:${subjectKey}`,\n RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN,\n 60_000\n )\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_subject\" });\n return;\n }\n const result = ingestRhizohProductOutcomeHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n schemaVersion: \"1.1.0\",\n aggregate: result.aggregate,\n integrity: result.integrity\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"outcome_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.productOutcomeAggregate) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome_agg:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const u = new URL(String(req.url || \"/\"), \"http://localhost\");\n const cohortId = u.searchParams.get(\"cohortId\") || \"\";\n const decisionFingerprint = u.searchParams.get(\"decisionFingerprint\") || \"\";\n const limit = u.searchParams.get(\"limit\") || \"50\";\n const rows = listRhizohProductOutcomeAggregates({\n cohortId: cohortId || undefined,\n decisionFingerprint: decisionFingerprint || undefined,\n limit: Number(limit) || 50\n });\n sendJson(res, 200, { ok: true, schemaVersion: \"1.1.0\", rows });\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.externalLossBatch) {\n try {\n if (process.env.RHIZOH_EXTERNAL_LOSS_BATCH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_loss_batch:${ip}`, RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const result = ingestRhizohExternalLossBatchHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, { ok: true, accepted: result.accepted ?? 0 });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"batch_failed\") });\n }\n return;\n }\n\n function moderationAdminOk(req) {\n const expected = String(process.env.CASTLE_MODERATION_ADMIN_KEY || \"\").trim();\n if (!expected) return false;\n const got = String(req.headers[\"x-castle-moderation-key\"] || \"\").trim();\n return got.length > 0 && got === expected;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.opsAbuseReport) {\n try {\n const body = await readHttpJson(req, 32 * 1024);\n const auth = await resolveHttpUser(req);\n const entry = submitAbuseReportV0({\n reporterUid: auth.ok ? auth.uid : null,\n targetUid: body?.targetUid ?? null,\n category: body?.category,\n detail: body?.detail,\n traceId: body?.traceId ?? null\n });\n sendJson(res, 202, { ok: true, reportId: entry.id, status: entry.status });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsModerationQueue) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n queue: listModerationQueueV0(100),\n softBlocked: listSoftBlockedUsersV0()\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-block\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const out = softBlockUserV0(body?.uid, body?.reason);\n sendJson(res, out.ok ? 200 : 400, out);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-unblock\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n sendJson(res, 200, removeSoftBlockV0(body?.uid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsAgentSnapshots) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n const limit = Number(new URL(req.url || \"\", \"http://local\").searchParams.get(\"limit\") || 50);\n sendJson(res, 200, { ok: true, snapshots: listRecentAgentSnapshotsV0(limit) });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.academicObservatoryExport) {\n const auth = await resolveHttpUser(req);\n const out = handleAcademicObservatoryExportGetV0(req, auth);\n sendJson(res, out.status, out.body);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsHardeningStatus) {\n const auth = await resolveHttpUser(req);\n const principal = auth.ok ? `uid:${auth.uid}` : null;\n const gclSnap = await getGlobalCostLedgerSnapshotV0(principal);\n const economicStrategy = evaluateEconomicStrategyL3V0(gclSnap);\n const hardeningTenantId = resolveHardeningTenantIdV0(auth, req);\n const unifiedState = await buildUnifiedStateNarrativeV0({\n tenantId: hardeningTenantId ?? undefined,\n platformScope: !hardeningTenantId,\n principal\n });\n sendJson(res, 200, {\n ok: true,\n schema: \"rhizoh.ops.hardening_status.v0\",\n containment: readAgentContainmentConfigV0(),\n cost: readCostContainmentConfigV0(),\n gcl: {\n backend: await getGclBackendStatusV0(),\n snapshot: gclSnap\n },\n economicStrategyL3: economicStrategy,\n phasedRollout: await getPhasedRolloutStatsV0(),\n tier: readPhasedRolloutTierV0(),\n principalCost: auth.ok ? await getCostContainmentStatsV0(principal) : null,\n stressTaxonomy: verifyStressTaxonomyCoverageV0(),\n stressConflictResolution: verifyStressConflictResolutionV0(),\n resolutionStabilityEnvelope: verifyResolutionStabilityEnvelopeV0(),\n unifiedState: {\n tenantScope: {\n tenantId: unifiedState.tenantScope?.tenantId,\n scope: unifiedState.tenantScope?.scope,\n noGlobalDerivedState: unifiedState.tenantScope?.noGlobalDerivedState\n },\n narrativeFingerprint: unifiedState.narrativeFingerprint,\n screenshotScopeWatermark: unifiedState.screenshotScopeWatermark,\n interpretationSafetyContract: unifiedState.interpretationSafetyContract,\n interpretationUxContract: {\n schema: unifiedState.interpretationUxContract?.schema,\n layerOrder: unifiedState.interpretationUxContract?.layerOrder,\n narrativeNeverVisuallyDominant: unifiedState.interpretationUxContract?.narrativeNeverVisuallyDominant\n },\n governance: {\n narrativeIsNotDecisionLayer: unifiedState.governance.narrativeIsNotDecisionLayer,\n displayOrder: unifiedState.governance.displayOrder,\n trustPosture: unifiedState.governance.trustPosture,\n interpretationUxContract: unifiedState.governance.interpretationUxContract?.schema\n },\n humanOps: {\n decisionTier: unifiedState.humanOps?.humanDecisionScaling?.tier,\n decisionOwner: unifiedState.humanOps?.humanDecisionScaling?.decisionOwner,\n opsRouteId: unifiedState.humanOps?.humanDecisionOpsRunbook?.routing?.routeId,\n slaAckMinutes: unifiedState.humanOps?.humanDecisionOpsRunbook?.sla?.ackMinutes,\n uiDisclaimerRequired: unifiedState.humanOps?.narrativeUiSafety?.displayRules?.requireDisclaimerBanner,\n misreadHighResidual: unifiedState.humanOps?.misreadSimulation?.highResidualCount,\n propagationHighResidual: unifiedState.humanOps?.socialPropagationSimulation?.highResidualCount,\n worstWatermarkSurvivability:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.worstWatermarkSurvivability,\n dominantDistortionSource:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.dominantDistortionSource\n },\n culturalRisk: {\n perceptualRiskClass: unifiedState.culturalRisk?.perceptualRiskClass,\n trustFork: unifiedState.culturalRisk?.trustDynamics?.fork,\n trustDecayScore: unifiedState.culturalRisk?.trustDynamics?.scores?.trustDecay,\n mythologyScore: unifiedState.culturalRisk?.trustDynamics?.scores?.mythology,\n confidenceDetachmentExposure:\n unifiedState.culturalRisk?.trustDynamics?.axes?.confidenceDetachmentExposure\n },\n appliedSystemsLayer: {\n schema: unifiedState.appliedSystemsLayer?.schema,\n researchOutputType: unifiedState.appliedSystemsLayer?.researchGovernance?.outputType,\n roboticsBlockActuation: unifiedState.appliedSystemsLayer?.roboticsGrounding?.blockActuation,\n spiralRumorCount:\n unifiedState.appliedSystemsLayer?.spiralMMOGameKernel?.gameLayer?.rumors?.length\n },\n actionSemanticGovernance: {\n schema: unifiedState.actionSemanticGovernance?.schema,\n highRiskActionCount: unifiedState.actionSemanticGovernance?.highRiskActionCount,\n loopClosed: unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.loopClosed,\n authorityCollapseMitigated:\n unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.risks?.authorityCollapse\n ?.mitigated\n },\n actionContextResolution: {\n schema: unifiedState.actionContextResolution?.schema,\n contextFingerprint: unifiedState.actionContextResolution?.contextFingerprint?.fingerprintHash,\n domainCollision: unifiedState.actionContextResolution?.domainCollision?.detected,\n deployAgentSpiralBlocked:\n unifiedState.actionContextResolution?.actionOverloadingRisk?.deployAgentExample\n ?.spiral_mmo === \"blocked\",\n roboticsActuationEligible:\n unifiedState.actionContextResolution?.roboticsActuationCorrectness?.actuationEligible\n },\n epistemicCoherence: {\n schema: unifiedState.epistemicCoherence?.schema,\n verdict: unifiedState.epistemicCoherence?.systemCoherence?.verdict,\n coherenceScore: unifiedState.epistemicCoherence?.systemCoherence?.score,\n contradictionCount: unifiedState.epistemicCoherence?.crossLayerContradictions?.count,\n operatorDecision: unifiedState.epistemicCoherence?.uxCompression?.operatorDecision,\n headlineTr: unifiedState.epistemicCoherence?.uxCompression?.headline?.tr,\n falseSafetyWarning: Boolean(\n unifiedState.epistemicCoherence?.uxCompression?.falseSafetyWarning\n )\n },\n coherenceAuthorityBoundary: {\n schema: unifiedState.coherenceAuthorityBoundary?.schema,\n bindingValid: unifiedState.coherenceAuthorityBoundary?.eclAuthorityBinding?.valid,\n canExecute: unifiedState.coherenceAuthorityBoundary?.contract?.can_execute,\n mandatoryBannerTr:\n unifiedState.coherenceAuthorityBoundary?.disclaimers?.mandatoryBanner?.tr\n },\n decisionLatencyGovernance: {\n schema: unifiedState.decisionLatencyGovernance?.schema,\n latencyTier: unifiedState.decisionLatencyGovernance?.latencyTier,\n inflationScore: unifiedState.decisionLatencyGovernance?.latencyInflation?.score,\n fastPathEligible: unifiedState.decisionLatencyGovernance?.fastPath?.eligible,\n primaryActionId: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.primaryActionId,\n ackSlaMinutes: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.ackSlaMinutes,\n certaintyCap: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.certaintyCap,\n uncertaintyItems:\n unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.uncertaintyEnvelope?.itemCount\n },\n decisionPacketUncertaintyBoundary: {\n schema: unifiedState.decisionPacketUncertaintyBoundary?.schema,\n invariantValid: unifiedState.decisionPacketUncertaintyBoundary?.invariantCheck?.valid,\n centralInvariant: unifiedState.decisionPacketUncertaintyBoundary?.centralInvariant\n },\n epistemicDecisionPacing: {\n schema: unifiedState.epistemicDecisionPacing?.schema,\n queueSaturationTier:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation?.tier,\n operatorProcessingLatency:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation\n ?.operatorProcessingLatency,\n postureWindowCount:\n unifiedState.epistemicDecisionPacing?.temporaryStaticPostureWindows\n ?.temporaryStaticPostureWindows?.windowCount,\n executionBoundariesValid:\n unifiedState.epistemicDecisionPacing?.executionBoundaries?.valid,\n grantsExecutionApproval: unifiedState.epistemicDecisionPacing?.grantsExecutionApproval\n },\n epistemicTemporalCoherence: {\n schema: unifiedState.epistemicTemporalCoherence?.schema,\n allInvariantsValid: unifiedState.epistemicTemporalCoherence?.allInvariantsValid,\n temporalAlignmentValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.temporalAlignment?.valid,\n crossWindowGuardValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard?.valid,\n delayedTruthValid: unifiedState.epistemicTemporalCoherence?.invariants?.delayedTruthRule?.valid,\n reconciliationRequired:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard\n ?.reconciliation?.required\n },\n realityDriftObserver: {\n schema: unifiedState.realityDriftObserver?.schema,\n driftActive: unifiedState.realityDriftObserver?.driftObserverSummary?.active,\n misapprehensionShapeCount:\n unifiedState.realityDriftObserver?.misapprehensionShapeCatalog?.shapeCount,\n criticalShapeCount:\n unifiedState.realityDriftObserver?.driftObserverSummary?.criticalShapeCount,\n roboticsMismatchActive:\n unifiedState.realityDriftObserver?.roboticsFeedbackMismatch?.active\n },\n driftCausality: {\n schema: unifiedState.driftCausality?.schema,\n shapesWithCausalExplanation: unifiedState.driftCausality?.rdolLinkage?.shapesWithCausalExplanation,\n activeDomainCount: unifiedState.driftCausality?.causalitySummary?.activeDomainCount,\n propagationCausalityActive: unifiedState.driftCausality?.causalDomains?.propagation?.active\n },\n productReadiness: unifiedState.productReadiness,\n stateLayers: {\n raw: unifiedState.stateLayers.raw,\n derived: {\n systemState: unifiedState.stateLayers.derived.systemState,\n narrative: unifiedState.stateLayers.derived.narrative,\n validation: unifiedState.stateLayers.derived.validation\n },\n policy: {\n binding: unifiedState.stateLayers.policy.binding,\n decisionOwner: unifiedState.stateLayers.policy.decisionOwner,\n prohibitedActions: unifiedState.stateLayers.policy.prohibitedActions\n }\n },\n systemState: unifiedState.systemState,\n interpretation: unifiedState.interpretation,\n validation: {\n trustPosture: unifiedState.validation.trustPosture,\n adjustedConfidence: unifiedState.validation.adjustedConfidence,\n agreement: unifiedState.validation.confidenceDecomposition.agreement,\n divergenceScore: unifiedState.validation.divergence.divergenceScore,\n validated: unifiedState.validation.divergence.validated,\n uncertaintyTags: unifiedState.validation.uncertainty.tags.map((t) => t.id),\n flags: unifiedState.validation.divergence.flags.map((f) => f.id)\n },\n compression: unifiedState.compression\n }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req, 6 * 1024 * 1024);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (\n !checkHttpRateLimit(`rhizoh_voice_transcribe:${rlKey}`, RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN, 60_000)\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const voiceEnv = rhizohVoiceTranscribeEnvV3();\n if (!voiceEnv.any) {\n sendJson(res, 503, {\n ok: false,\n error: \"voice_asr_not_configured\",\n env: voiceEnv\n });\n return;\n }\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n const result = await runRhizohVoiceTranscribeV3(\n applyLanguagePropagationToVoiceBodyV1(payload, langProp)\n );\n sendJson(res, result.ok ? 200 : 422, {\n ...result,\n traceId: String(payload?.traceId || \"\"),\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n atMs: Date.now()\n });\n } catch (error) {\n sendJson(res, 500, {\n ok: false,\n error: \"voice_transcribe_failed\",\n detail: String(error?.message || error),\n ...buildRhizohLanguagePropagationEchoV1(langProp)\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === rhizohRuntime.routes.rhizohLlm) {\n let payload = {};\n let langProp = parseRhizohLanguagePropagationV1(req, {});\n try {\n payload = await readHttpJson(req);\n langProp = parseRhizohLanguagePropagationV1(req, payload);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n logRhizohHealth(\"gateway_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: String(payload?.traceId || \"\"),\n auth: auth.ok ? \"ok\" : \"anon\",\n ip\n });\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (!checkHttpRateLimit(`rhizoh_llm:${rlKey}`, RL_RHIZOH_LLM_PER_MIN, 60_000)) {\n const rlTaxonomy = classifyStressResponseV0({ code: \"rate_limit_exceeded\" });\n return sendJson(res, 429, {\n ok: false,\n error: \"rate_limit_exceeded\",\n reply: rlTaxonomy.userMessageTr || \"İstek sınırı aşıldı. Kısa süre sonra tekrar deneyin.\",\n stressClass: rlTaxonomy.stressClass,\n responseAction: rlTaxonomy.responseAction,\n stressMatrix: rlTaxonomy.matrix,\n stressInterpretable: rlTaxonomy.interpretable,\n stressConfidence: rlTaxonomy.stressConfidence,\n actionConfidence: rlTaxonomy.actionConfidence,\n actionSoftened: rlTaxonomy.actionSoftened,\n responseActionStrict: rlTaxonomy.responseActionStrict,\n actionInterpretable: rlTaxonomy.actionInterpretable,\n conflictResolution: rlTaxonomy.conflictResolution,\n directive: \"NONE\"\n });\n }\n\n const requireExplicit = process.env.CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE === \"1\";\n const keyMode = normalizeClientLlmKeySource(payload?.llmKeySource ?? payload?.keySource);\n if (keyMode === null) {\n return sendJson(res, 400, {\n ok: false,\n error: \"invalid_llm_key_source\",\n reply: \"llmKeySource geçersiz. Kullanın: env | user_connection | auto\",\n directive: \"NONE\"\n });\n }\n if (requireExplicit && keyMode === \"auto\") {\n return sendJson(res, 400, {\n ok: false,\n error: \"llm_key_source_required\",\n reply: \"Sunucu CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE=1: istekte llmKeySource (env veya user_connection) zorunlu.\",\n directive: \"NONE\"\n });\n }\n\n const conn = auth.ok ? await resolveConnection(auth.uid, String(payload?.connectionId || \"\")) : null;\n\n if (keyMode === \"user_connection\") {\n if (!auth.ok) {\n return sendJson(res, 401, {\n ok: false,\n error: \"llm_auth_required_for_user_connection\",\n reply: \"Kayıtlı LLM bağlantısı için giriş gerekli.\",\n directive: \"NONE\"\n });\n }\n if (!conn?.apiKey) {\n return sendJson(res, 400, {\n ok: false,\n error: \"user_llm_connection_required\",\n reply: \"Geçerli bir LLM bağlantısı yok; Studio’da bağlantı ekleyin veya llmKeySource=env kullanın.\",\n directive: \"NONE\"\n });\n }\n }\n\n if (langProp.partialPropagation) {\n logRhizohHealth(\"language_propagation_partial\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: langProp.traceId || String(payload?.traceId || \"\"),\n gaps: langProp.propagationGaps\n });\n }\n const safePayload = applyLanguagePropagationToLlmPayloadV1(\n { ...(payload || {}) },\n langProp\n );\n delete safePayload.apiKey;\n delete safePayload.llmKeySource;\n delete safePayload.keySource;\n\n let resolvedProvider = payload?.provider;\n let resolvedModel = payload?.model;\n let connApiKey = \"\";\n\n if (keyMode === \"user_connection\") {\n resolvedProvider = conn.provider;\n resolvedModel = conn.model;\n connApiKey = conn.apiKey || \"\";\n } else if (keyMode === \"env\") {\n connApiKey = \"\";\n } else {\n resolvedProvider = conn?.provider || payload?.provider;\n resolvedModel = conn?.model || payload?.model;\n connApiKey = conn?.apiKey || \"\";\n }\n\n const { result, traceId, turnLatencyMs, spinePhases, sampledTrace } = await rhizohGatewayTurn({\n safePayload,\n auth,\n keyMode,\n conn,\n resolvedProvider,\n resolvedModel,\n connApiKey\n });\n logRhizohHealth(\"llm_response\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId,\n turnLatencyMs: Number(turnLatencyMs || 0),\n keyMode\n });\n sendJson(res, 200, {\n ...result,\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n traceId,\n turnLatencyMs,\n ...(spinePhases ? { spinePhases } : {}),\n ...(sampledTrace !== undefined ? { sampledTrace } : {})\n });\n } catch (error) {\n const msg = String(error?.message || \"\");\n const code = error?.code || \"\";\n let status = 500;\n if (msg === \"rate_limit_exceeded\") status = 429;\n else if (\n code === \"cost_hard_limit\" ||\n code === \"phased_rollout_capacity\" ||\n error?.containment === true ||\n String(code).startsWith(\"agent_\")\n ) {\n status = 429;\n } else if (code === \"user_soft_blocked\" || code === \"prompt_abuse_detected\") status = 403;\n else if ([\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(code) || msg.includes(\"missing_api_key_for_\"))\n status = 400;\n let rhizohFailureKind = \"provider_error\";\n if (msg === \"rate_limit_exceeded\" || status === 429) rhizohFailureKind = \"rate_limit\";\n else if (status === 403) rhizohFailureKind = \"policy_block\";\n else if (status === 400) rhizohFailureKind = \"client_config\";\n else if (msg.startsWith(\"provider_http_\")) {\n const st = Number(msg.replace(\"provider_http_\", \"\"));\n if (st === 408 || st === 504) rhizohFailureKind = \"timeout\";\n else rhizohFailureKind = \"provider_error\";\n } else if (/timeout|timed out|ETIMEDOUT/i.test(msg)) rhizohFailureKind = \"timeout\";\n\n const providerHttpMatch = msg.match(/^provider_http_(\\d{3})$/);\n const stressTaxonomy =\n error?.stressClass != null\n ? {\n stressClass: error.stressClass,\n responseAction: error.responseAction,\n stressMatrix: error.stressMatrix,\n stressInterpretable: error.stressInterpretable !== false,\n stressConfidence: error.stressConfidence,\n actionConfidence: error.actionConfidence,\n actionSoftened: error.actionSoftened,\n responseActionStrict: error.responseActionStrict,\n actionInterpretable: error.actionInterpretable,\n conflictResolution: error.conflictResolution,\n stressSecondary: error.stressSecondary,\n responseActions: error.responseActions\n }\n : (() => {\n const t = classifyStressResponseV0({\n code: code || msg,\n rhizohFailureKind,\n providerHttpStatus: providerHttpMatch\n ? Number(providerHttpMatch[1])\n : null\n });\n return {\n stressClass: t.stressClass,\n responseAction: t.responseAction,\n stressMatrix: t.matrix,\n stressInterpretable: t.interpretable,\n stressConfidence: t.stressConfidence,\n actionConfidence: t.actionConfidence,\n actionSoftened: t.actionSoftened,\n responseActionStrict: t.responseActionStrict,\n actionInterpretable: t.actionInterpretable,\n conflictResolution: t.conflictResolution,\n stressSecondary: t.stressSecondary,\n responseActions: t.responseActions\n };\n })();\n sendJson(res, status, {\n ok: false,\n rhizohFailureKind,\n ...(providerHttpMatch ? { providerHttpStatus: Number(providerHttpMatch[1]) } : {}),\n error: code || msg || \"rhizoh_llm_failed\",\n detail: msg || code || undefined,\n reply:\n error?.reply ||\n (status === 500 ? \"Rhizoh bağlantısı geçici olarak kesildi.\" : msg),\n directive: error?.directive || \"NONE\",\n ...buildRhizohLanguagePropagationEchoV1(langProp),\n ...stressTaxonomy\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicSeal) {\n try {\n if (!EPISTEMIC_SEAL_SECRET) {\n sendJson(res, 503, { ok: false, error: \"epistemic_seal_disabled\" });\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`epistemic_seal:${ip}`, RL_EPISTEMIC_SEAL_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const truth = body.truth_contract;\n if (!truth || typeof truth !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"truth_contract_required\" });\n return;\n }\n const sealBody = {\n truth_contract: truth,\n runtime_hash: String(body.runtime_hash || \"\"),\n model_route: {\n provider: body.model_route?.provider ?? null,\n model: body.model_route?.model ?? null\n },\n memory_digest: String(body.memory_digest || \"\"),\n world_snapshot_hash: String(body.world_snapshot_hash || \"\"),\n timestamp: Number(body.timestamp) || Date.now()\n };\n const canonical = canonicalEpistemicSealString(sealBody);\n const { hash, signature } = hashAndSignEpistemicSeal(canonical, EPISTEMIC_SEAL_SECRET);\n recordGenesisEpistemicSealIssued(hash);\n const attestation = {\n schema: EPISTEMIC_SEAL_SCHEMA,\n algorithm: \"SHA-256+HMAC-SHA256\",\n keyHint: process.env.CASTLE_EPISTEMIC_SEAL_SECRET ? \"CASTLE_EPISTEMIC_SEAL_SECRET\" : \"CASTLE_GATEWAY_TOKEN\",\n issuedAt: Date.now(),\n gateway: { service: \"castle-gateway\", listenPort: PORT }\n };\n sendJson(res, 200, {\n ok: true,\n hash,\n signature,\n attestation,\n canonicalBytes: Buffer.byteLength(canonical, \"utf8\")\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"seal_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicLogsBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`epistemic_logs:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 120) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const persisted = await persistEpistemicLedgerBatch(auth.uid, entries);\n recordGenesisEpistemicLedgerPersisted(persisted.persisted);\n // Internal cognition layer: forecast stays gateway/ledger-only (no client exposure).\n try {\n await persistEpistemicForecastBatch(auth.uid, persisted.normalized || []);\n } catch {\n /* forecast failures must not break primary ledger ingest */\n }\n sendJson(res, 200, {\n ok: true,\n persisted: persisted.persisted,\n mode: persisted.mode,\n latest: persisted.normalized?.[persisted.normalized.length - 1] || null\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"epistemic_log_ingest_failed\") });\n }\n return;\n }\n\n sendJson(res, 404, { ok: false, error: \"not_found\" });\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\nspiralWssForGenesis = wss;\nconst orchestrator = createOrchestrator();\nconst clientStats = new Map();\nlet broadcasterClientId = null;\nconst spiralState = {\n rooms: [{ id: \"room-default\", name: \"Spiral Default Arena\" }],\n activeByClient: new Map(),\n characters: new Map() // id -> {id,name,role,roomId,source}\n};\n\n/** Castle social rooms — keyed by `castleRoomKey` from {@link WS_MESSAGE.CASTLE_SOCIAL_PULSE}. */\nconst castleSocialRoomState = new Map();\nconst CASTLE_SOCIAL_MEMBER_TTL_MS = 90_000;\nconst CASTLE_SOCIAL_MAX_PULSE_BYTES = 8192;\n\nfunction removeCastleSocialClientFromAllRooms(clientId) {\n const cid = String(clientId || \"\");\n if (!cid) return;\n for (const room of castleSocialRoomState.values()) {\n room.members.delete(cid);\n }\n}\n\nfunction buildCastleSocialRoomPayload(roomKey) {\n const room = castleSocialRoomState.get(roomKey);\n if (!room) return null;\n const now = Date.now();\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const roster = [...room.members.values()].map((m) => ({\n userId: m.userId,\n lastMs: m.lastMs,\n ...m.sp\n }));\n const peerFeeds =\n room.walPeers && room.walPeers.size\n ? [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }))\n : [];\n return { castleRoomKey: roomKey, seq: room.seq, roster, peerFeeds, ts: now };\n}\n\nconst CASTLE_WAL_PEER_MAX_FEED_BYTES = 48 * 1024;\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {{ ok: boolean, code?: string, reason?: string }} gate\n */\nfunction rejectWalPeerIngressV0(socket, gate) {\n const code = gate.code || \"wal_peer_rejected\";\n recordWalPeerRejectV0(code);\n recordSubstrateOtelEventV0(\"wal_peer_reject\", { code, reason: String(gate.reason || \"\").slice(0, 120) });\n socket.send(\n JSON.stringify(\n createEnvelope(WS_MESSAGE.ERROR, {\n error: gate.reason || \"WAL peer ingress rejected.\",\n code\n })\n )\n );\n}\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {Record<string, unknown> | null | undefined} walPeerFeed\n * @returns {boolean}\n */\nfunction gateWalPeerIngressV0(socket, walPeerFeed) {\n const authGate = assertWalPeerSocketAuthorityV0(socket);\n if (!authGate.ok) {\n rejectWalPeerIngressV0(socket, authGate);\n return false;\n }\n const sigGate = validateWalPeerFeedSignatureV0(\n walPeerFeed && typeof walPeerFeed === \"object\" ? walPeerFeed : {}\n );\n if (!sigGate.ok) {\n rejectWalPeerIngressV0(socket, sigGate);\n return false;\n }\n return true;\n}\n\nfunction handleCastleWalPeerFeed(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_WAL_PEER_MAX_FEED_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_WAL_PEER_FEED too large.\" })));\n return;\n }\n const walPeerFeed = p.walPeerFeed && typeof p.walPeerFeed === \"object\" ? p.walPeerFeed : {};\n if (!gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const castleRoomKey = String(p.castleRoomKey || \"wal:convergence\").slice(0, 64);\n const castleId = String(p.castleId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId,\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: room.members.get(socket.clientId)?.sp ?? {}\n });\n const peerFeeds = [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }));\n const body = {\n castleRoomKey,\n seq: room.seq,\n peerFeeds,\n ts: now\n };\n const env = createEnvelope(WS_MESSAGE.CASTLE_WAL_PEER_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys(), ...room.walPeers.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction handleCastleSocialPulse(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_SOCIAL_MAX_PULSE_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_SOCIAL_PULSE too large.\" })));\n return;\n }\n const castleRoomKey = String(p.castleRoomKey || \"default\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n const sp = p.socialPulse && typeof p.socialPulse === \"object\" ? p.socialPulse : {};\n const walPeerFeed =\n p.walPeerFeed && typeof p.walPeerFeed === \"object\"\n ? p.walPeerFeed\n : sp.walPeerFeed && typeof sp.walPeerFeed === \"object\"\n ? sp.walPeerFeed\n : null;\n if (walPeerFeed && !gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const spNext = walPeerFeed ? { ...sp, walPeerFeed } : { ...sp };\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: spNext\n });\n if (walPeerFeed) {\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId: String(p.castleId || userId).slice(0, 64),\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n }\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const body = buildCastleSocialRoomPayload(castleRoomKey);\n if (!body) return;\n const env = createEnvelope(WS_MESSAGE.CASTLE_SOCIAL_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcast(msgObj) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcastState() {\n broadcast(\n createEnvelope(WS_MESSAGE.BROADCAST_STATE, {\n broadcasterClientId,\n viewers: [...wss.clients].filter((c) => c.readyState === 1).length\n })\n );\n}\n\nfunction currentSpiralStatePayload() {\n return {\n rooms: spiralState.rooms,\n characters: [...spiralState.characters.values()]\n };\n}\n\nfunction broadcastSpiralState() {\n broadcast(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload()));\n}\n\nfunction ensureRoom(roomId, fallbackName = null) {\n if (!roomId) return spiralState.rooms[0].id;\n const exists = spiralState.rooms.find((r) => r.id === roomId);\n if (exists) return exists.id;\n const created = { id: roomId, name: fallbackName || `Spiral-${roomId.slice(-4)}` };\n spiralState.rooms.push(created);\n return created.id;\n}\n\nfunction syncSpiralCharactersWithWorld(snapshot) {\n const agents = snapshot?.agents || [];\n for (const a of agents) {\n if (!spiralState.characters.has(a.id)) {\n spiralState.characters.set(a.id, {\n id: a.id,\n name: a.id,\n role: \"agent\",\n roomId: spiralState.rooms[0].id,\n source: \"world\"\n });\n }\n }\n}\n\nfunction broadcastExcept(msgObj, exceptSocket) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client !== exceptSocket && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction validateInputFrame(payload, socket) {\n const stats = clientStats.get(socket.clientId) || { lastFrame: 0, windowStart: Date.now(), msgCount: 0 };\n const now = Date.now();\n if (now - stats.windowStart > 5000) {\n stats.windowStart = now;\n stats.msgCount = 0;\n }\n stats.msgCount += 1;\n clientStats.set(socket.clientId, stats);\n if (stats.msgCount > 120) return \"Rate limit exceeded.\";\n\n if (!payload || !Number.isInteger(payload.frame)) return \"frame must be integer.\";\n if (payload.frame <= stats.lastFrame) return \"frame must be monotonic.\";\n const commands = Array.isArray(payload.commands) ? payload.commands : [];\n if (commands.length > 8) return \"Too many commands in one frame.\";\n for (const cmd of commands) {\n if (!cmd || typeof cmd.command !== \"string\") return \"Invalid command format.\";\n if (\n cmd.command !== \"SPAWN_AGENT\" &&\n cmd.command !== \"LIST_CASTLES\" &&\n cmd.command !== \"SPAWN_ENTITY\" &&\n cmd.command !== \"CREATE_CASTLE\"\n ) return \"Unsupported command.\";\n }\n stats.lastFrame = payload.frame;\n clientStats.set(socket.clientId, stats);\n return null;\n}\n\nfunction validateSignalPayload(payload) {\n if (!payload || typeof payload !== \"object\") return \"Invalid signaling payload.\";\n const signalType = payload.signalType;\n if (![\"OFFER\", \"ANSWER\", \"ICE\"].includes(signalType)) return \"Invalid signal type.\";\n if (typeof payload.to !== \"string\" || payload.to.length < 3) return \"Invalid signal destination.\";\n if (signalType !== \"ICE\" && JSON.stringify(payload.sdp || {}).length > 16 * 1024) return \"SDP too large.\";\n if (signalType === \"ICE\" && JSON.stringify(payload.candidate || {}).length > 6 * 1024) return \"ICE too large.\";\n return null;\n}\n\nfunction sanitizeRawMessage(raw) {\n if (!raw) return null;\n const text = raw.toString();\n if (Buffer.byteLength(text, \"utf8\") > MAX_MESSAGE_BYTES) return null;\n return text;\n}\n\nwss.on(\"connection\", async (socket, req) => {\n const origin = req?.headers?.origin || \"\";\n if (!httpCors.isWebSocketOriginAllowed(origin)) {\n socket.close(1008, \"Origin not allowed\");\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const path = req?.url || \"\";\n const query = new URL(`ws://localhost${path}`).searchParams;\n const token = query.get(\"token\");\n if (!token || token !== REQUIRED_GATEWAY_TOKEN) {\n socket.close(1008, \"Invalid gateway token\");\n return;\n }\n }\n if (REQUIRE_AUTH) {\n const result = await verifyClientToken(req);\n if (!result.ok) {\n const isLocal = origin.includes(\"localhost\") || origin.includes(\"127.0.0.1\") || !origin;\n if (!(ALLOW_DEV_ANON && isLocal)) {\n socket.close(1008, result.reason);\n return;\n }\n socket.auth = { ok: true, kind: \"dev-anon\", user: { uid: \"dev-anon\" } };\n } else {\n socket.auth = result;\n }\n }\n\n socket.clientId = `c-${Math.random().toString(36).slice(2, 8)}`;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.HELLO, { message: \"Castle Gateway online.\", clientId: socket.clientId })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.WORLD_SNAPSHOT, orchestrator.worldSnapshot())));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.BROADCAST_STATE, { broadcasterClientId, viewers: [...wss.clients].filter((c) => c.readyState === 1).length })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload())));\n broadcastExcept(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }), socket);\n\n socket.on(\"message\", (raw) => {\n const safeRaw = sanitizeRawMessage(raw);\n if (!safeRaw) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Message too large or invalid.\" })));\n return;\n }\n const parsed = safeJsonParse(safeRaw);\n if (!parsed?.type) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid message.\" })));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND_TEXT) {\n const canonical = parseTextToCommand(parsed.payload?.text);\n if (!canonical) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Unknown command text.\" })));\n return;\n }\n const result = orchestrator.applyCommand(canonical);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND) {\n const result = orchestrator.applyCommand(parsed.payload);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n }\n\n if (parsed.type === WS_MESSAGE.INPUT_FRAME) {\n const err = validateInputFrame(parsed.payload, socket);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const frame = parsed.payload.frame;\n const commands = Array.isArray(parsed.payload?.commands) ? parsed.payload.commands : [];\n orchestrator.queueInputFrame(socket.clientId, frame, commands);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, queued: true, frame })));\n }\n\n if (parsed.type === WS_MESSAGE.SIGNAL) {\n const payload = parsed.payload || {};\n const err = validateSignalPayload(payload);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const signalType = payload.signalType;\n const to = payload.to;\n for (const client of wss.clients) {\n if (client.clientId === to && client.readyState === 1) {\n client.send(JSON.stringify(createEnvelope(WS_MESSAGE.SIGNAL, { ...payload, from: socket.clientId })));\n break;\n }\n }\n }\n\n if (parsed.type === WS_MESSAGE.DRONE_TELEMETRY) {\n const payload = parsed.payload || {};\n const json = JSON.stringify(payload);\n if (Buffer.byteLength(json, \"utf8\") > 48 * 1024) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"DRONE_TELEMETRY payload too large.\" })));\n return;\n }\n broadcastExcept(createEnvelope(WS_MESSAGE.DRONE_TELEMETRY, { ...payload, relayFrom: socket.clientId }), socket);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_SOCIAL_PULSE) {\n handleCastleSocialPulse(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_WAL_PEER_FEED) {\n handleCastleWalPeerFeed(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.OPEN_DATA_QUERY) {\n queryOpenData(parsed.payload)\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: true, result })));\n })\n .catch((e) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: false, error: e.message })));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_QUERY) {\n runRhizohBrainV2({\n ...(parsed.payload || {}),\n uid: socket?.auth?.user?.uid || \"unknown\"\n })\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, result)));\n })\n .catch(async () => {\n const fallback = await runRhizohBrain(parsed.payload || {}).catch(() => ({\n ok: false,\n reply: \"Rhizoh temporary disturbance. Habitat remains stable.\"\n }));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, fallback)));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.BROADCAST_REGISTER) {\n const role = String(parsed.payload?.role || \"\");\n if (role === \"GENESIS_BROADCAST_AGENT\") {\n broadcasterClientId = socket.clientId;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, registered: role })));\n broadcastState();\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.STUDIO_CUE) {\n // Only registered broadcaster can push global cues.\n if (!broadcasterClientId || socket.clientId !== broadcasterClientId) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Only broadcaster agent can emit STUDIO_CUE.\" })));\n return;\n }\n const cue = parsed.payload || {};\n if (typeof cue !== \"object\") {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid STUDIO_CUE payload.\" })));\n return;\n }\n broadcast(createEnvelope(WS_MESSAGE.STUDIO_CUE, cue));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_JOIN_ROOM) {\n const requested = String(parsed.payload?.roomId || \"\").trim();\n const requestedName = String(parsed.payload?.roomName || \"\").trim();\n const roomId = ensureRoom(requested || `room-${Math.random().toString(36).slice(2, 7)}`, requestedName || null);\n spiralState.activeByClient.set(socket.clientId, roomId);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, roomId })));\n broadcastSpiralState();\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_CREATE_CHARACTER) {\n const name = String(parsed.payload?.name || \"\").trim().slice(0, 32) || `AI-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;\n const role = String(parsed.payload?.role || \"agent\").trim().slice(0, 24) || \"agent\";\n const roomId = ensureRoom(\n String(parsed.payload?.roomId || spiralState.activeByClient.get(socket.clientId) || spiralState.rooms[0].id),\n null\n );\n\n // Character creation always maps to an authoritative world agent.\n const result = orchestrator.applyCommand({\n command: COMMAND.SPAWN_AGENT,\n params: { targetCastleId: \"castle-01\" }\n });\n if (!result?.ok || !result?.data?.id) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Character creation failed.\" })));\n return;\n }\n const id = result.data.id;\n spiralState.characters.set(id, { id, name, role, roomId, source: \"spawned\" });\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, characterId: id, roomId })));\n broadcastSpiralState();\n return;\n }\n });\n\n socket.on(\"close\", () => {\n if (socket.clientId === broadcasterClientId) broadcasterClientId = null;\n clientStats.delete(socket.clientId);\n spiralState.activeByClient.delete(socket.clientId);\n removeCastleSocialClientFromAllRooms(socket.clientId);\n broadcast(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }));\n broadcastState();\n });\n});\n\nsetInterval(() => {\n void (async () => {\n try {\n const { frame, snapshot, delta, packedDelta } = orchestrator.tick(1 / 20);\n syncSpiralCharactersWithWorld(snapshot);\n broadcast(createEnvelope(WS_MESSAGE.NETWORK_TICK, { frame }));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_TICK, snapshot));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA, delta));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA_PACKED, packedDelta));\n broadcastSpiralState();\n await runAcademyEventTick();\n const executed = await tickAutomations(Date.now());\n for (const a of executed) {\n await addTranscript(a.uid, {\n source: \"planner\",\n eventType: \"automation-executed\",\n text: `Automation executed: ${a.title} (${a.action})`,\n roomId: \"studio-main\",\n meta: { automationId: a.id, action: a.action, deviceId: a.deviceId || \"\" }\n });\n }\n } catch (e) {\n console.error(\"[GATEWAY] world tick / automations loop error:\", String(e?.message || e));\n }\n })();\n}, 50);\n\n(async () => {\n await rhizohRuntime.telemetry.initOpenTelemetry();\n if (String(process.env.CASTLE_GENESIS_DISK_PERSIST ?? \"\").trim() === \"\") {\n process.env.CASTLE_GENESIS_DISK_PERSIST = \"1\";\n }\n const genesisDiskBoot = await hydrateGenesisContinuityPersistenceBootV0();\n if (!genesisDiskBoot.ok) {\n console.warn(\"[GATEWAY] genesis disk hydrate failed; continuing with in-memory genesis continuity\");\n }\n httpServer.listen(PORT, () => {\n startGenesisCanonicalClock();\n installGenesisCheckpointSurfaceGetter(buildGenesisRuntimeSurfacePayloadLive);\n setGenesisContinuityAfterPublishHook((seq) => {\n noteGenesisCheckpointSeqCommitted(seq);\n noteGenesisContinuitySeqAcceptedV0(seq);\n });\n startGenesisContinuityInfraSampler(buildGenesisRuntimeSurfacePayloadLive, 2000);\n logProductionObservatorySurfaceGuardsV0();\n logGatewaySubstrateAuthorityGuardsV0();\n console.log(`[GATEWAY] ws/http://localhost:${PORT}`);\n console.log(\n `[GATEWAY] genesis continuity: GET ${rhizohRuntime.routes.genesisRuntime} | POST ${rhizohRuntime.routes.genesisIngress} | SSE ${rhizohRuntime.routes.genesisStream} | replay ${rhizohRuntime.routes.genesisReplay} | diff ${rhizohRuntime.routes.genesisReplayDiff} | equiv ${rhizohRuntime.routes.genesisReplayEquivalence} | analytics ${rhizohRuntime.routes.genesisReplayAnalytics} | evolution ${rhizohRuntime.routes.genesisReplayEvolution}`\n );\n if (String(process.env.CASTLE_GENESIS_OBSERVATORY_LIVE_V01 ?? \"\").trim() === \"1\") {\n console.log(\"GENESIS OBSERVATORY LIVE v0.1 — READ ONLY PHASE STARTED\");\n }\n });\n})().catch((e) => {\n console.error(\"[GATEWAY] fatal boot (before or during listen):\", e);\n process.exit(1);\n});\n"}, "4c43a25a83df5b192af2629c6268cab31932853e": {"t": 1785754162.3776946, "n": 153486, "h": "335aae96d9f5f361", "c": "import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport dotenv from \"dotenv\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconsole.log(\"🔥 [GENESIS_BOOT] entry file:\", __filename);\nconsole.log(\"🔥 [GENESIS_BOOT] node env:\", process.env.NODE_ENV);\nconsole.log(\"🔥 [GENESIS_BOOT] pid:\", process.pid);\nconsole.log(\"🧬 [GENESIS_BOOT] cwd:\", process.cwd());\nconsole.log(\"🧬 [GENESIS_BOOT] import.meta.url:\", import.meta.url);\ndotenv.config({ path: path.join(__dirname, \"..\", \".env\") });\ndotenv.config({ path: path.join(__dirname, \"..\", \".env.local\"), override: true });\n\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport { createServer } from \"node:http\";\nimport { createOrchestrator } from \"@castle/orchestrator\";\nimport { parseTextToCommand } from \"@castle/command-dsl\";\nimport { WS_MESSAGE, COMMAND, createEnvelope, safeJsonParse } from \"@castle/protocol\";\nimport { queryOpenData } from \"./openData.js\";\nimport { verifyClientToken } from \"./auth.js\";\nimport { runRhizohBrain } from \"./rhizohBrain.js\";\nimport { queryRhizohLlm } from \"./rhizohLlmGateway.js\";\nimport { rhizohGatewayTurn } from \"./rhizohGatewayTurn.js\";\nimport {\n RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n runRhizohVoiceTranscribeV3,\n rhizohVoiceTranscribeEnvV3\n} from \"./rhizohVoiceTranscribeV3.js\";\nimport { createHttpCorsPolicy, normalizeHttpOrigin, parseAllowedOriginsFromEnv } from \"./httpCorsPolicyV1.js\";\nimport {\n meshAppendDelta,\n meshContinuityAggregate,\n meshJoin,\n meshLeave,\n meshReplayFromNodeId,\n meshReplayFromSeq,\n meshSnapshot,\n meshSubscribeSse\n} from \"./presenceRoomMesh.js\";\nimport { runRhizohBrainV2 } from \"./rhizohBrainV2.js\";\nimport {\n listConnections,\n createConnection,\n updateConnection,\n deleteConnection,\n setDefaultConnection,\n resolveConnection\n} from \"./llmConnectionsStore.js\";\nimport { checkHttpRateLimit, getHttpClientIp } from \"./castleHttpRateLimit.js\";\nimport {\n submitAbuseReportV0,\n listModerationQueueV0,\n softBlockUserV0,\n removeSoftBlockV0,\n listSoftBlockedUsersV0\n} from \"./ops/moderationMvpV0.js\";\nimport { listRecentAgentSnapshotsV0 } from \"./ops/agentObservabilityV0.js\";\nimport { readAgentContainmentConfigV0 } from \"./ops/agentContainmentV0.js\";\nimport { readCostContainmentConfigV0, getCostContainmentStatsV0 } from \"./ops/costContainmentV0.js\";\nimport { getGlobalCostLedgerSnapshotV0, getGclBackendStatusV0 } from \"./ops/globalCostLedgerV0.js\";\nimport { evaluateEconomicStrategyL3V0 } from \"./ops/economicStrategyEngineL3V0.js\";\nimport { getPhasedRolloutStatsV0, readPhasedRolloutTierV0 } from \"./ops/phasedRolloutV0.js\";\nimport { buildUnifiedStateNarrativeV0 } from \"./ops/unifiedStateNarrativeV0.js\";\nimport { resolveHardeningTenantIdV0 } from \"./ops/narrativeTenantIsolationV0.js\";\nimport {\n classifyStressResponseV0,\n verifyStressTaxonomyCoverageV0,\n verifyStressConflictResolutionV0\n} from \"./ops/stressResponseTaxonomyV0.js\";\nimport { verifyResolutionStabilityEnvelopeV0 } from \"./ops/resolutionStabilityEnvelopeV0.js\";\nimport { logLlmAccess } from \"./castleLlmAudit.js\";\nimport { appendMemory, listMemories, getMemoryContext, getPersonaGoalMemory, setPersonaGoalMemory, autoCompactMemories } from \"./memoryStore.js\";\nimport { getFirebasePersistence } from \"./firebasePersistence.js\";\nimport { registerAgentIdentity, listAgentIdentities, getAgentIdentity, updateAgentIdentity } from \"./agentIdentityStore.js\";\nimport { queueAcademyEvent, listAcademyEvents, resolveAcademyEvent, runAcademyEventTick } from \"./academyEventEngine.js\";\nimport { enqueuePdfJob, getPdfJob } from \"./pdfPipeline.js\";\nimport {\n createPublishSession,\n listPublishSessions,\n updatePublishSession,\n addTranscript,\n listTranscripts,\n searchMetadata,\n registerDevice,\n listDevices,\n updateDevice,\n appendTelemetry,\n listTelemetry,\n createAutomation,\n listAutomations,\n tickAutomations,\n enqueueCommand,\n listCommands,\n updateCommandStatus,\n registerSocialChannel,\n listSocialChannels\n} from \"./studioOpsStore.js\";\nimport { metrics as infraMetrics } from \"./infra/metrics.js\";\nimport { renderRhizohPrometheusMetrics, rhizohEnterpriseMetrics } from \"./infra/rhizohEnterpriseMetrics.js\";\nimport { scoreHealth } from \"./infra/healthScore.js\";\nimport { getTraceById, listTracesBySession } from \"./infra/traceRegistry.js\";\nimport { canonicalEpistemicSealString, hashAndSignEpistemicSeal, EPISTEMIC_SEAL_SCHEMA } from \"./epistemicSeal.js\";\nimport { persistEpistemicLedgerBatch } from \"./epistemicLedgerStore.js\";\nimport { persistEpistemicForecastBatch } from \"./epistemicForecastStore.js\";\nimport { buildRhizohExternalGroundTruthPayload } from \"./rhizohExternalGroundTruthGateway.js\";\nimport { ingestRhizohExternalLossBatchHttp } from \"./rhizohExternalLossIngestGateway.js\";\nimport {\n ingestRhizohProductOutcomeHttp,\n listRhizohProductOutcomeAggregates,\n verifyRhizohOutcomeHmac,\n verifyRhizohOutcomeSourceToken\n} from \"./rhizohProductOutcomeIngestGateway.js\";\nimport { initRhizoh } from \"./rhizohProductionBootstrap.js\";\nimport { handleAcademicObservatoryExportGetV0 } from \"./rhizoh/academicObservatoryHttpV0.js\";\nimport {\n buildGenesisRuntimeSurfacePayload,\n recordGenesisEpistemicLedgerPersisted,\n recordGenesisEpistemicSealIssued,\n startGenesisCanonicalClock\n} from \"./genesisRuntimeSurfaceV0.js\";\nimport { registerGenesisSseClient, getGenesisContinuitySeq, setGenesisContinuityAfterPublishHook } from \"./genesisContinuityStreamHubV0.js\";\nimport { computeGenesisReplayFingerprintV0 } from \"./genesisReplayFingerprintV0.js\";\nimport { buildGenesisGatewayCapabilitiesV0 } from \"./genesisGatewayCapabilitiesV0.js\";\nimport {\n installGenesisCheckpointSurfaceGetter,\n noteGenesisCheckpointSeqCommitted,\n getLatestGenesisSignedCheckpoint\n} from \"./genesisContinuityCheckpointV0.js\";\nimport { hydrateGenesisContinuityPersistenceBootV0 } from \"./genesisContinuityHydrateBootV0.js\";\nimport { startGenesisContinuityInfraSampler } from \"./genesisContinuityInfraSamplerV0.js\";\nimport {\n genesisCheckpointQueryBySeqV0,\n genesisCheckpointQueryRangeV0,\n genesisCheckpointQueryLineageV0\n} from \"./genesisCheckpointQueryV0.js\";\nimport {\n genesisContinuityEventArchiveEnabled,\n queryGenesisContinuityEventArchiveV0,\n GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION\n} from \"./genesisContinuityEventArchiveV0.js\";\nimport { resolveGenesisReplayRouterV1, GENESIS_REPLAY_ROUTER_SCHEMA } from \"./genesisReplayRouterV1.js\";\nimport {\n computeGenesisReplayTemporalDiffV1,\n GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA\n} from \"./genesisReplayTemporalDiffV1.js\";\nimport { compareGenesisReplayEquivalenceV1 } from \"./genesisReplayEquivalenceV1.js\";\nimport { computeGenesisReplayAnalyticsV1 } from \"./genesisReplayAnalyticsV1.js\";\nimport { computeGenesisReplayEvolutionV1 } from \"./genesisReplayEvolutionV1.js\";\nimport { handleStripeCheckoutCreate, handleStripeMembershipWebhook } from \"./stripeWebhookMembership.js\";\nimport { buildRealityHealthPayload } from \"./edgeRealityConsistencyV1.js\";\nimport {\n assertWalPeerSocketAuthorityV0,\n buildGatewaySubstrateAuthoritySnapshotV0,\n logGatewaySubstrateAuthorityGuardsV0,\n validateWalPeerFeedSignatureV0\n} from \"./gatewaySubstrateAuthorityV0.js\";\nimport {\n buildSubstrateOperationalSnapshotV0,\n recordWalPeerRejectV0,\n renderSubstratePrometheusMetricsV0\n} from \"./infra/substrateOperationalMetrics.js\";\nimport { ingestClientSubstrateHealthV0 } from \"./rhizohSubstrateHealthIngestV0.js\";\nimport { recordSubstrateOtelEventV0 } from \"./infra/opentelemetrySubstrateV0.js\";\nimport {\n GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n getGenesisWorldObservationIngressStatusV0,\n ingestGenesisWorldObservationV0\n} from \"./genesisContinuityClientIngressV0.js\";\nimport {\n getGenesisContinuitySeqAuditV0,\n noteGenesisContinuitySeqAcceptedV0\n} from \"./genesisContinuitySeqGapV0.js\";\n\n/** Render / Fly / Railway: `PORT` — yoksa `CASTLE_GATEWAY_PORT` — yoksa 8090. */\nconst PORT =\n Number(process.env.PORT) ||\n Number(process.env.CASTLE_GATEWAY_PORT) ||\n 8090;\n/** Spiral / world WebSocket server — set after `wss` init; used by genesis runtime surface. */\nlet spiralWssForGenesis = null;\nconst MAX_MESSAGE_BYTES = Number(process.env.CASTLE_MAX_MESSAGE_BYTES || 32 * 1024);\nconst REQUIRED_GATEWAY_TOKEN = process.env.CASTLE_GATEWAY_TOKEN || \"\";\nconst ALLOWED_ORIGINS = parseAllowedOriginsFromEnv().fromList;\nconst httpCors = createHttpCorsPolicy();\n\n/** @returns {string|null} `Access-Control-Allow-Origin`; null = browser CORS fail. */\nfunction accessControlAllowOriginValue(req) {\n return httpCors.accessControlAllowOriginValue(req);\n}\n\nfunction applyHttpCorsHeaders(req, res) {\n return httpCors.applyHttpCorsHeaders(req, res);\n}\nconst REQUIRE_AUTH = process.env.CASTLE_REQUIRE_AUTH === \"true\";\nconst ALLOW_DEV_ANON = process.env.CASTLE_ALLOW_DEV_ANON !== \"false\";\nconst ALLOW_DEV_HTTP_UID = process.env.CASTLE_ALLOW_DEV_HTTP_UID !== \"false\";\n/** Presence mesh: allow `X-Castle-Guest-Id` when Firebase auth absent (default on). Set `false` to require auth only. */\nconst ALLOW_MESH_GUEST = process.env.CASTLE_ALLOW_MESH_GUEST !== \"false\";\nconst SOCIAL_RETRY_MAX = Math.max(0, Math.min(5, Number(process.env.CASTLE_SOCIAL_RETRY_MAX || 2)));\nconst SOCIAL_RETRY_BASE_MS = Math.max(100, Number(process.env.CASTLE_SOCIAL_RETRY_BASE_MS || 700));\nconst TELEGRAM_BOT_TOKEN = process.env.CASTLE_TELEGRAM_BOT_TOKEN || \"\";\nconst WHATSAPP_TOKEN = process.env.CASTLE_WHATSAPP_TOKEN || \"\";\nconst WHATSAPP_PHONE_NUMBER_ID = process.env.CASTLE_WHATSAPP_PHONE_NUMBER_ID || \"\";\nconst RL_RHIZOH_LLM_PER_MIN = Math.max(5, Number(process.env.CASTLE_RL_RHIZOH_LLM_PER_MIN || 40));\nconst RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN || 30)\n);\nconst RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN || 120));\nconst RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN = Math.max(8, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN || 48));\nconst RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN = Math.max(4, Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN || 24));\nconst RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN || 18)\n);\nconst RL_EPISTEMIC_SEAL_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_EPISTEMIC_SEAL_PER_MIN || 120));\nconst RL_EPISTEMIC_LOG_PER_MIN = Math.max(20, Number(process.env.CASTLE_RL_EPISTEMIC_LOG_PER_MIN || 240));\nconst EPISTEMIC_SEAL_SECRET = String(\n process.env.CASTLE_EPISTEMIC_SEAL_SECRET || process.env.CASTLE_GATEWAY_TOKEN || \"\"\n).trim();\nconst RL_LLM_CONN_TEST_PER_MIN = Math.max(2, Number(process.env.CASTLE_RL_LLM_CONNECTION_TEST_PER_MIN || 8));\n\n/** One-shot production warnings: CORS surface, auth toggle, dev bypass flags (observatory deploy hygiene). */\nfunction logProductionObservatorySurfaceGuardsV0() {\n if (String(process.env.NODE_ENV || \"\").toLowerCase() !== \"production\") return;\n const tag = \"[GATEWAY_PROD_GUARD]\";\n const primaryCors = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n const corsListEmpty = ALLOWED_ORIGINS.length === 0;\n if (corsListEmpty && !primaryCors) {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty and CASTLE_HTTP_CORS_ORIGIN empty — production still merges RHIZOH_BROWSER_ORIGINS_V1 defaults; set env for preview/staging hosts`\n );\n } else if (corsListEmpty && primaryCors && primaryCors !== \"*\") {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty — only CASTLE_HTTP_CORS_ORIGIN set; add web.app + firebaseapp.com (and previews) to CASTLE_ALLOWED_ORIGINS if clients use alternate hostnames`\n );\n }\n if (primaryCors && /localhost|127\\.0\\.0\\.1/i.test(primaryCors)) {\n console.warn(`${tag} CORS: CASTLE_HTTP_CORS_ORIGIN is localhost while NODE_ENV=production`);\n }\n if (REQUIRE_AUTH) {\n console.warn(\n `${tag} AUTH: CASTLE_REQUIRE_AUTH=true — clients must send a valid gateway credential (otherwise silent 401 on replay/SSE); EventSource cannot set custom headers unless proxied`\n );\n if (REQUIRED_GATEWAY_TOKEN.length < 16) {\n console.warn(`${tag} AUTH: CASTLE_GATEWAY_TOKEN missing or short while require-auth enabled`);\n }\n }\n if (ALLOW_DEV_ANON) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_ANON is not \"false\" — dev anonymous path may be live in production (set CASTLE_ALLOW_DEV_ANON=false)`\n );\n }\n if (ALLOW_DEV_HTTP_UID) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_HTTP_UID is not \"false\" — X-Castle-Dev-Uid bypass risk (set CASTLE_ALLOW_DEV_HTTP_UID=false)`\n );\n }\n}\n\nconst rhizohRuntime = initRhizoh();\n\n/** İstemci: llmKeySource veya keySource — env | user_connection | auto */\nfunction normalizeClientLlmKeySource(raw) {\n const s = String(raw ?? \"\").trim().toLowerCase();\n if (s === \"env\" || s === \"server\") return \"env\";\n if (s === \"user_connection\" || s === \"connection\" || s === \"user\") return \"user_connection\";\n if (s === \"auto\" || s === \"\") return \"auto\";\n return null;\n}\n\nfunction sendJson(res, statusCode, payload) {\n res.writeHead(statusCode, { \"Content-Type\": \"application/json; charset=utf-8\" });\n res.end(JSON.stringify(payload));\n}\n\nfunction logRhizohHealth(stage, detail = {}) {\n try {\n console.info(`[CASTLE_${String(stage || \"unknown\")}]`, detail && typeof detail === \"object\" ? detail : {});\n } catch {\n /* noop */\n }\n}\n\nfunction renderInfraMetricsProm() {\n const lines = [\n `castle_gateway_events_processed_total ${infraMetrics.eventsProcessed}`,\n `castle_gateway_duplicate_reject_total ${infraMetrics.duplicateRejects}`,\n `castle_gateway_enqueue_latency_ms ${infraMetrics.enqueueLatencyMs}`,\n `castle_gateway_queue_depth ${infraMetrics.queueDepth}`,\n `castle_gateway_queue_lag_seconds ${(Number(infraMetrics.queueLag || 0) / 1000).toFixed(3)}`,\n `castle_gateway_errors_total ${infraMetrics.errors}`\n ];\n const buckets = infraMetrics.enqueueLatencyBuckets || {};\n for (const k of Object.keys(buckets)) {\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"${k}\"} ${buckets[k]}`);\n }\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"+Inf\"} ${infraMetrics.eventsProcessed}`);\n if (process.env.CASTLE_SUBSTRATE_METRICS !== \"0\") {\n lines.push(renderSubstratePrometheusMetricsV0().trimEnd());\n }\n return lines.join(\"\\n\");\n}\n\nasync function fetchJsonWithTimeout(url, timeoutMs = 1200) {\n const ctl = new AbortController();\n const tid = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const res = await fetch(url, { signal: ctl.signal });\n if (!res.ok) return null;\n return await res.json();\n } catch {\n return null;\n } finally {\n clearTimeout(tid);\n }\n}\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction getHttpPathname(req) {\n let p = String(req.url || \"\").split(\"?\")[0];\n /* Reverse proxies / clients sometimes append `/` — avoid silent 404 on e.g. `/rhizoh/genesis/stream/`. */\n while (p.length > 1 && p.endsWith(\"/\")) {\n p = p.slice(0, -1);\n }\n return p;\n}\n\nfunction rhizohLlmEnvConfigured() {\n const p = String(process.env.CASTLE_LLM_PROVIDER || \"openai\").toLowerCase();\n const key =\n p === \"anthropic\"\n ? process.env.ANTHROPIC_API_KEY\n : p === \"gemini\"\n ? process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY\n : p === \"xai\"\n ? process.env.XAI_API_KEY\n : p === \"deepseek\"\n ? process.env.DEEPSEEK_API_KEY\n : p === \"mistral\"\n ? process.env.MISTRAL_API_KEY\n : p === \"openrouter\"\n ? process.env.OPENROUTER_API_KEY\n : process.env.OPENAI_API_KEY;\n return !!String(key || \"\").trim();\n}\n\n/**\n * @param {import(\"firebase-admin/firestore\").Firestore | null} db\n * @param {number} timeoutMs\n */\nasync function probeFirestoreReachable(db, timeoutMs = 2000) {\n if (!db) return { ok: false, reason: \"no_db\" };\n const timeout = sleep(timeoutMs).then(() => ({ ok: false, reason: \"timeout\" }));\n const work = db\n .collection(\"_castle_gateway_health\")\n .doc(\"ping\")\n .get()\n .then(() => ({ ok: true, reason: \"reachable\" }))\n .catch((e) => ({ ok: false, reason: String(e?.message || e || \"firestore_error\") }));\n return Promise.race([work, timeout]);\n}\n\nfunction stripHtml(text = \"\") {\n return String(text)\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \" \")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \" \")\n .replace(/<[^>]+>/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction haversineMeters(aLat, aLon, bLat, bLon) {\n const R = 6371000;\n const dLat = ((bLat - aLat) * Math.PI) / 180;\n const dLon = ((bLon - aLon) * Math.PI) / 180;\n const sa = Math.sin(dLat / 2);\n const sb = Math.sin(dLon / 2);\n const x = sa * sa + Math.cos((aLat * Math.PI) / 180) * Math.cos((bLat * Math.PI) / 180) * sb * sb;\n return 2 * R * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));\n}\n\nasync function checkPublishTargetHealth(protocol, target) {\n const p = String(protocol || \"\").toUpperCase();\n const t = String(target || \"\").trim();\n if (!t) return { ok: false, reason: \"target_missing\", shouldReconnect: false };\n if (p === \"RTMP\") {\n const ok = t.startsWith(\"rtmp://\") || t.startsWith(\"rtmps://\");\n return { ok, reason: ok ? \"rtmp_target_format_ok\" : \"invalid_rtmp_target\", shouldReconnect: !ok, reconnectInSec: 15 };\n }\n if (p === \"WHIP\" || p === \"SFU\") {\n try {\n const res = await fetch(t, { method: \"OPTIONS\" });\n const ok = res.status < 500;\n return { ok, reason: `http_${res.status}`, shouldReconnect: !ok, reconnectInSec: 8 };\n } catch {\n return { ok: false, reason: \"network_unreachable\", shouldReconnect: true, reconnectInSec: 8 };\n }\n }\n return { ok: true, reason: \"unchecked_protocol\", shouldReconnect: false };\n}\n\nasync function fetchTextFromUrl(url) {\n const res = await fetch(url, { method: \"GET\" });\n if (!res.ok) throw new Error(`fetch_${res.status}`);\n const ct = String(res.headers.get(\"content-type\") || \"\");\n if (ct.includes(\"application/pdf\")) throw new Error(\"pdf_binary_not_supported_directly\");\n const body = await res.text();\n return stripHtml(body).slice(0, 14000);\n}\n\nfunction buildTaskActionGraph(task = \"\", context = {}) {\n const t = String(task || \"\").toLowerCase();\n if (t.includes(\"dinner\") || t.includes(\"akşam yemeği\") || t.includes(\"aksam yemegi\")) {\n return {\n taskTitle: \"Dinner Prep Planner\",\n nodes: [\n { id: \"n1\", title: \"Kitchen preheat\", action: \"smart-oven.preheat\", params: { temperatureC: 190, mode: \"fan\" } },\n { id: \"n2\", title: \"Lighting comfort\", action: \"smart-light.set_scene\", params: { scene: \"warm_dinner\" } },\n { id: \"n3\", title: \"Fridge inventory check\", action: \"smart-fridge.inventory_scan\", params: {} },\n { id: \"n4\", title: \"Reminder notification\", action: \"smart-watch.notify\", params: { text: \"Dinner prep started.\" } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" },\n { from: \"n3\", to: \"n4\" }\n ]\n };\n }\n return {\n taskTitle: task || \"General Planner\",\n nodes: [\n { id: \"n1\", title: \"Safety check\", action: \"device.safety_check\", params: { context } },\n { id: \"n2\", title: \"Primary action\", action: \"device.execute_task\", params: { task } },\n { id: \"n3\", title: \"Report\", action: \"studio.timeline_note\", params: { task } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" }\n ]\n };\n}\n\nfunction buildEthicsPrograms() {\n return {\n policyVersion: \"v1\",\n tracks: [\n {\n id: \"elderly-care-assist\",\n audience: \"elderly\",\n title: \"Elderly Assistance Track\",\n principles: [\"human-in-the-loop\", \"consent-first\", \"low-friction alerts\"],\n modules: [\"medication-reminder\", \"fall-risk-check\", \"family-notify\", \"daily-companion-brief\"]\n },\n {\n id: \"kids-safe-learning\",\n audience: \"children\",\n title: \"Kids Safe Learning Track\",\n principles: [\"guardian-consent\", \"age-appropriate-language\", \"content-filtering\"],\n modules: [\"study-coach\", \"screen-time-balance\", \"safe-social-share\", \"emergency-contact\"]\n }\n ]\n };\n}\n\nasync function deliverViaTelegram(channel, text) {\n const chatId = String(channel.endpoint || \"\").trim();\n if (!TELEGRAM_BOT_TOKEN) throw new Error(\"telegram_token_missing\");\n if (!chatId) throw new Error(\"telegram_chat_id_missing\");\n const url = `https://api.telegram.org/bot${encodeURIComponent(TELEGRAM_BOT_TOKEN)}/sendMessage`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ chat_id: chatId, text })\n });\n if (!res.ok) throw new Error(`telegram_http_${res.status}`);\n return { provider: \"telegram\", status: res.status };\n}\n\nasync function deliverViaWhatsapp(channel, text) {\n const to = String(channel.endpoint || \"\").trim();\n if (!WHATSAPP_TOKEN) throw new Error(\"whatsapp_token_missing\");\n if (!WHATSAPP_PHONE_NUMBER_ID) throw new Error(\"whatsapp_phone_number_id_missing\");\n if (!to) throw new Error(\"whatsapp_to_missing\");\n const url = `https://graph.facebook.com/v22.0/${encodeURIComponent(WHATSAPP_PHONE_NUMBER_ID)}/messages`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${WHATSAPP_TOKEN}` },\n body: JSON.stringify({ messaging_product: \"whatsapp\", to, type: \"text\", text: { body: text } })\n });\n if (!res.ok) throw new Error(`whatsapp_http_${res.status}`);\n return { provider: \"whatsapp\", status: res.status };\n}\n\nasync function deliverViaWebhook(channel, text) {\n const endpoint = String(channel.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"webhook_endpoint_missing\");\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text, platform: channel.platform || \"webhook\", ts: Date.now() })\n });\n if (!res.ok) throw new Error(`webhook_http_${res.status}`);\n return { provider: \"webhook\", status: res.status };\n}\n\nasync function deliverSocialMessage(channel, text) {\n const platform = String(channel.platform || \"webhook\").toLowerCase();\n if (platform === \"telegram\") return deliverViaTelegram(channel, text);\n if (platform === \"whatsapp\") return deliverViaWhatsapp(channel, text);\n return deliverViaWebhook(channel, text);\n}\n\nasync function deliverSocialWithRetry(channel, text) {\n let attempt = 0;\n let lastError = null;\n while (attempt <= SOCIAL_RETRY_MAX) {\n try {\n const result = await deliverSocialMessage(channel, text);\n return { ok: true, attempt: attempt + 1, ...result };\n } catch (error) {\n lastError = error;\n if (attempt >= SOCIAL_RETRY_MAX) break;\n const jitter = Math.floor(Math.random() * 90);\n await sleep(SOCIAL_RETRY_BASE_MS * Math.pow(2, attempt) + jitter);\n }\n attempt += 1;\n }\n return { ok: false, attempt: attempt + 1, error: String(lastError?.message || \"delivery_failed\") };\n}\n\nasync function dispatchCommandToAdapter(device, command) {\n const adapter = String(command?.adapter || device?.adapter || \"websocket\").toLowerCase();\n const endpoint = String(device?.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"device_endpoint_missing\");\n if (adapter === \"mqtt\") {\n // MQTT delivery is proxied via a bridge endpoint (HTTP webhook style).\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n type: \"mqtt-command\",\n topic: `castle/devices/${device.id}/commands`,\n action: command.action,\n params: command.params || {}\n })\n });\n if (!res.ok) throw new Error(`mqtt_bridge_http_${res.status}`);\n return { ok: true, adapter, via: \"http-bridge\", status: res.status };\n }\n if (!endpoint.startsWith(\"ws://\") && !endpoint.startsWith(\"wss://\")) throw new Error(\"invalid_websocket_endpoint\");\n const sendResult = await new Promise((resolve, reject) => {\n const ws = new WebSocket(endpoint);\n const timer = setTimeout(() => {\n try {\n ws.close();\n } catch {\n /* noop */\n }\n reject(new Error(\"websocket_timeout\"));\n }, 3000);\n ws.on(\"open\", () => {\n ws.send(\n JSON.stringify({\n type: \"castle-command\",\n commandId: command.id,\n deviceId: command.deviceId,\n action: command.action,\n params: command.params || {}\n }),\n (err) => {\n clearTimeout(timer);\n ws.close();\n if (err) reject(new Error(\"websocket_send_failed\"));\n else resolve({ ok: true, adapter: \"websocket\", via: \"ws\" });\n }\n );\n });\n ws.on(\"error\", () => {\n clearTimeout(timer);\n reject(new Error(\"websocket_connect_failed\"));\n });\n });\n return sendResult;\n}\n\nfunction readHttpJson(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => {\n if (!buf) return resolve({});\n const payload = safeJsonParse(buf);\n if (!payload || typeof payload !== \"object\") return reject(new Error(\"invalid_json_payload\"));\n resolve(payload);\n });\n req.on(\"error\", reject);\n });\n}\n\n/** Ham gövde (HMAC doğrulaması için). */\nfunction readHttpBodyText(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => resolve(buf));\n req.on(\"error\", reject);\n });\n}\n\nfunction maskOpaqueId(value) {\n const t = String(value || \"\");\n if (!t) return \"(empty)\";\n if (t.length <= 8) return \"****\";\n return `${t.slice(0, 4)}…${t.slice(-2)}`;\n}\n\nfunction readGatewayHttpToken(req) {\n const x = String(req.headers?.[\"x-castle-gateway-token\"] || \"\").trim();\n if (x) return x;\n const auth = String(req.headers?.authorization || \"\");\n const m = /^Bearer\\s+(.+)$/i.exec(auth);\n return m ? String(m[1] || \"\").trim() : \"\";\n}\n\nasync function resolveHttpUser(req) {\n const authResult = await verifyClientToken(req);\n if (authResult?.ok && authResult.user?.uid) {\n return { ok: true, uid: String(authResult.user.uid), mode: authResult.kind || \"auth\" };\n }\n if (ALLOW_DEV_ANON && ALLOW_DEV_HTTP_UID) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) return { ok: true, uid: `dev-${devUid.slice(0, 64)}`, mode: \"dev-header\" };\n }\n return { ok: false, reason: authResult?.reason || \"auth_required\" };\n}\n\n/**\n * Epistemic ledger ingest — Firebase uid OR valid transport token + X-Castle-Dev-Uid.\n * Prod disables CASTLE_ALLOW_DEV_HTTP_UID; LLM may still work while telemetry 401'd without this.\n */\nasync function resolveEpistemicIngestActor(req) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok && tok === REQUIRED_GATEWAY_TOKEN) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) {\n return { ok: true, uid: `gw-telemetry-${devUid.slice(0, 64)}`, mode: \"gateway-token-dev-uid\" };\n }\n return { ok: false, reason: \"dev_uid_required\" };\n }\n if (tok) {\n return { ok: false, reason: \"gateway_token_mismatch\" };\n }\n }\n const auth = await resolveHttpUser(req);\n if (auth.ok) return auth;\n return { ok: false, reason: auth.reason || \"auth_required\" };\n}\n\n/** Mesh actor: Firebase uid, dev header, or `guest:<uuid>` via header / `guestId` query (SSE cannot set headers). */\nasync function resolveMeshActor(req) {\n const auth = await resolveHttpUser(req);\n if (auth.ok) return { ok: true, clientUid: auth.uid, mode: auth.mode };\n if (ALLOW_MESH_GUEST) {\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const gid = String(req.headers?.[\"x-castle-guest-id\"] || q.searchParams.get(\"guestId\") || \"\").trim();\n if (gid) {\n const normalized = gid.startsWith(\"guest:\") ? gid.slice(0, 180) : `guest:${gid.slice(0, 160)}`;\n return { ok: true, clientUid: normalized, mode: \"guest-header\" };\n }\n }\n return { ok: false, reason: \"mesh_auth_required\" };\n}\n\n/** Live genesis runtime JSON (GET /rhizoh/genesis/runtime + SSE infra sampler). */\nasync function buildGenesisRuntimeSurfacePayloadLive() {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n let spiralWsActive = 0;\n if (spiralWssForGenesis) {\n for (const c of spiralWssForGenesis.clients) {\n if (c.readyState === 1) spiralWsActive += 1;\n }\n }\n const base = buildGenesisRuntimeSurfacePayload({\n infraMetrics,\n scoredHealth: scored,\n rhizohEnterpriseMetrics,\n mesh: meshContinuityAggregate(),\n workerHealth: worker,\n port: PORT,\n spiralWebSocketClientsActive: spiralWsActive\n });\n return {\n ...base,\n replayFingerprint: computeGenesisReplayFingerprintV0(base),\n gatewayCapabilities: buildGenesisGatewayCapabilitiesV0(),\n genesisStream: {\n schema: \"castle.genesis.stream_cursor.v0\",\n lastAcceptedSeq: getGenesisContinuitySeq(),\n clientIngress: {\n schema: GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n contract: \"castle.world_observation.ingress_envelope.v1\",\n postPath: rhizohRuntime.routes.genesisIngress,\n closure: getGenesisWorldObservationIngressStatusV0()\n },\n seqContinuity: getGenesisContinuitySeqAuditV0(),\n eventArchive: genesisContinuityEventArchiveEnabled()\n ? {\n enabled: true,\n projection: GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION,\n queryPath: rhizohRuntime.routes.genesisContinuityEvents\n }\n : { enabled: false },\n replayRouter: {\n schema: GENESIS_REPLAY_ROUTER_SCHEMA,\n queryPath: rhizohRuntime.routes.genesisReplay,\n queryHint: \"from=&to= or range=from-to; optional type=&checkpoints=0\",\n temporalDiffPath: rhizohRuntime.routes.genesisReplayDiff,\n temporalDiffSchema: GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA,\n equivalencePath: rhizohRuntime.routes.genesisReplayEquivalence,\n analyticsPath: rhizohRuntime.routes.genesisReplayAnalytics,\n evolutionPath: rhizohRuntime.routes.genesisReplayEvolution\n }\n }\n };\n}\n\nconst httpServer = createServer(async (req, res) => {\n const corsAllow = applyHttpCorsHeaders(req, res);\n if (req.method === \"OPTIONS\") {\n if (!corsAllow && req.headers?.origin) {\n console.warn(\"[GATEWAY_CORS] preflight rejected for origin:\", req.headers.origin);\n }\n res.writeHead(corsAllow ? 204 : 403);\n res.end();\n return;\n }\n\n const pathname = getHttpPathname(req);\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/genesis/__ping\") {\n sendJson(res, 200, {\n ok: true,\n pid: process.pid,\n entry: __filename,\n time: Date.now(),\n genesis: true\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/epistemic/__probe\") {\n sendJson(res, 200, {\n ok: true,\n epistemicIngestActorV2: true,\n routes: {\n seal: rhizohRuntime.routes.epistemicSeal,\n logsBatch: rhizohRuntime.routes.epistemicLogsBatch\n },\n hasGatewayToken: Boolean(REQUIRED_GATEWAY_TOKEN),\n gatewayTokenLen: REQUIRED_GATEWAY_TOKEN ? REQUIRED_GATEWAY_TOKEN.length : 0\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/webhooks/stripe\") {\n await handleStripeMembershipWebhook(req, res);\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/billing/stripe/checkout\") {\n await handleStripeCheckoutCreate(req, res, { readHttpJson, resolveHttpUser, sendJson });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/live\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, live: false, dns: true, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n sendJson(res, 200, { ok: true, live: true, dns: true, service: \"castle-gateway\", ts: Date.now() });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/cors\") {\n const reqOrigin = normalizeHttpOrigin(req.headers?.origin);\n const allowOrigin = accessControlAllowOriginValue(req);\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n requestOrigin: reqOrigin || null,\n allowed: Boolean(allowOrigin),\n allowOrigin: allowOrigin || null,\n allowSetSize: httpCors.allowSet.size\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/ready\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, ready: false, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n try {\n const persistence = getFirebasePersistence();\n const llmProbe = rhizohLlmEnvConfigured();\n sendJson(res, 200, {\n ok: true,\n ready: true,\n dns: true,\n persistence: persistence.mode,\n service: \"castle-gateway\",\n gateway: \"ok\",\n llm: llmProbe ? \"ok\" : \"degraded\",\n spine: llmProbe ? \"ok\" : \"partial\"\n });\n } catch (e) {\n sendJson(res, 503, { ok: false, ready: false, reason: String(e?.message || e), service: \"castle-gateway\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/reality\") {\n const payload = buildRealityHealthPayload();\n sendJson(res, payload.ok ? 200 : 503, payload);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/substrate\") {\n const authority = buildGatewaySubstrateAuthoritySnapshotV0();\n const operational = buildSubstrateOperationalSnapshotV0();\n const drift = buildRealityHealthPayload();\n const ok = Boolean(authority.productionAuthoritySatisfied) && drift.ok;\n sendJson(res, ok ? 200 : 503, {\n ok,\n service: \"castle-gateway\",\n ts: Date.now(),\n authority,\n operational,\n drift: { ok: drift.ok, errors: drift.drift?.errors, warnings: drift.drift?.warnings }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/substrate/health\") {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const body = await readHttpJson(req);\n const uid = (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestClientSubstrateHealthV0(body, { uid, clientId: uid });\n if (!result.ok) {\n sendJson(res, 400, result);\n return;\n }\n recordSubstrateOtelEventV0(\"client_health_ingest\", { uid: String(uid).slice(0, 32) });\n sendJson(res, 200, result);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/deps\") {\n const t0 = Date.now();\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, {\n ok: false,\n dns: true,\n llm: false,\n firestore: false,\n phase: \"maintenance\",\n latencyMs: Date.now() - t0,\n service: \"castle-gateway\"\n });\n return;\n }\n const persistence = getFirebasePersistence();\n let firestoreOk = persistence.mode !== \"firebase\";\n let firestoreDetail = persistence.mode === \"file\" ? \"file_mode\" : \"not_applicable\";\n if (persistence.mode === \"firebase\" && persistence.db) {\n const pr = await probeFirestoreReachable(persistence.db, 2000);\n firestoreOk = pr.ok;\n firestoreDetail = pr.reason || (pr.ok ? \"reachable\" : \"fail\");\n } else if (persistence.mode === \"firebase\" && !persistence.db) {\n firestoreOk = false;\n firestoreDetail = \"firebase_not_initialized\";\n }\n const llmOk = rhizohLlmEnvConfigured();\n const overallOk = llmOk && firestoreOk;\n const latencyMs = Date.now() - t0;\n sendJson(res, 200, {\n ok: overallOk,\n dns: true,\n llm: llmOk,\n firestore: persistence.mode === \"firebase\" ? firestoreOk : false,\n persistence: persistence.mode,\n firestoreDetail,\n latencyMs,\n service: \"castle-gateway\",\n wsPort: PORT\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health\") {\n const persistence = getFirebasePersistence();\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n wsPort: PORT,\n persistence: persistence.mode,\n presenceMesh: \"sse\"\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/health\") {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n sendJson(res, 200, {\n ok: true,\n role: \"gateway\",\n queueLagMs: infraMetrics.queueLag,\n queueDepth: infraMetrics.queueDepth,\n errors: infraMetrics.errors,\n status: scored.status,\n score: scored.score,\n reasons: scored.reasons,\n worker: worker\n ? {\n status: worker.status,\n score: worker.score,\n reasons: worker.reasons\n }\n : null,\n timestamp: Date.now()\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisRuntime) {\n const payload = await buildGenesisRuntimeSurfacePayloadLive();\n sendJson(res, 200, payload);\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.genesisIngress) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const actor = await resolveMeshActor(req);\n const clientId = actor.ok ? actor.clientUid : (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestGenesisWorldObservationV0(body, { clientId });\n if (!result.ok) {\n const status =\n result.error === \"rate_limited\" || result.error === \"agent_spoke_throttled\"\n ? 429\n : result.error === \"payload_too_large\"\n ? 413\n : 400;\n sendJson(res, status, result);\n return;\n }\n sendJson(res, result.idempotent ? 200 : 202, result);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisStream) {\n const corsOrigin = accessControlAllowOriginValue(req);\n if (!corsOrigin && String(process.env.NODE_ENV || \"\").toLowerCase() === \"production\") {\n const og = String(req.headers?.origin || \"\").trim();\n if (og) {\n console.warn(\"[GATEWAY] genesis SSE: missing Access-Control-Allow-Origin for browser Origin (CORS whitelist mismatch):\", og);\n }\n }\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n res.write(\"retry: 3000\\n\\n\");\n res.write(\": genesis continuity stream\\n\\n\");\n res.write(\"event: genesis\\n\");\n res.write(`data: {\"booted\":true,\"pid\":${process.pid}}\\n\\n`);\n registerGenesisSseClient(res, req);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLatest) {\n const cp = getLatestGenesisSignedCheckpoint();\n if (!cp) {\n sendJson(res, 404, { ok: false, error: \"no_checkpoint_yet\", hint: \"wait_for_seq_interval_or_configure_signing_secret\" });\n return;\n }\n sendJson(res, 200, { ok: true, checkpoint: cp });\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(rhizohRuntime.routes.genesisCheckpointBySeqPrefix)) {\n const tail = pathname.slice(rhizohRuntime.routes.genesisCheckpointBySeqPrefix.length).split(\"/\")[0];\n const seq = parseInt(String(tail || \"\").trim(), 10);\n const out = await genesisCheckpointQueryBySeqV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"checkpoint_not_found\") {\n sendJson(res, 404, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointRange) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryRangeV0(from, to);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"range_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLineage) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const seq = parseInt(String(u.searchParams.get(\"seq\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryLineageV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"lineage_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisContinuityEvents) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const limit = parseInt(String(u.searchParams.get(\"limit\") || \"\").trim(), 10);\n const out = await queryGenesisContinuityEventArchiveV0(from, to, type, limit);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\" || out.error === \"event_archive_disabled\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"archive_file_too_large\") {\n sendJson(res, 413, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplay) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await resolveGenesisReplayRouterV1({\n from: fromSeq,\n to: toSeq,\n type,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayDiff) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const out = await computeGenesisReplayTemporalDiffV1({ from: fromSeq, to: toSeq, type });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEquivalence) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from1 = parseInt(String(u.searchParams.get(\"from1\") || \"\").trim(), 10);\n const to1 = parseInt(String(u.searchParams.get(\"to1\") || \"\").trim(), 10);\n const from2 = parseInt(String(u.searchParams.get(\"from2\") || \"\").trim(), 10);\n const to2 = parseInt(String(u.searchParams.get(\"to2\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await compareGenesisReplayEquivalenceV1({\n from1,\n to1,\n from2,\n to2,\n type,\n includeCheckpoints\n });\n if (\n !out.ok &&\n (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")\n ) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayAnalytics) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayAnalyticsV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEvolution) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const collapseWindows = parseInt(String(u.searchParams.get(\"collapseWindows\") || \"0\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayEvolutionV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n collapseWindows,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/metrics\") {\n res.writeHead(200, { \"Content-Type\": \"text/plain; version=0.0.4; charset=utf-8\" });\n res.end(renderInfraMetricsProm());\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.infraPrometheusMetrics) {\n if (process.env.CASTLE_PROMETHEUS_METRICS !== \"1\") {\n sendJson(res, 404, { ok: false, error: \"prometheus_metrics_disabled\" });\n return;\n }\n res.writeHead(200, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(renderRhizohPrometheusMetrics());\n return;\n }\n if (req.method === \"GET\" && pathname.startsWith(\"/infra/traces/\")) {\n const traceId = decodeURIComponent(pathname.slice(\"/infra/traces/\".length));\n const item = await getTraceById(traceId);\n if (!item) return sendJson(res, 404, { ok: false, error: \"trace_not_found\" });\n return sendJson(res, 200, { ok: true, trace: item });\n }\n if (req.method === \"GET\" && pathname === \"/infra/traces\") {\n const u = new URL(String(req.url || \"/infra/traces\"), `http://${req.headers.host || \"localhost\"}`);\n const sessionId = String(u.searchParams.get(\"sessionId\") || \"\").trim();\n const items = sessionId ? await listTracesBySession(sessionId) : [];\n return sendJson(res, 200, { ok: true, sessionId, traces: items });\n }\n\n /** Presence room mesh (C): JOIN / LEAVE / SNAPSHOT / DELTA / REPLAY (HTTP); SUBSCRIBE = SSE stream. */\n if (req.method === \"POST\" && pathname === \"/presence/mesh/join\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid, clientUid: actor.clientUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/leave\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshLeave(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/snapshot\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n sendJson(res, 200, meshSnapshot(roomUid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/delta\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, MAX_MESSAGE_BYTES);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const seq = meshAppendDelta(roomUid, actor.clientUid, {\n node: body?.node,\n projectionPatch: body?.projectionPatch,\n writerSubject: body?.writerSubject\n });\n sendJson(res, 200, { ok: true, roomUid, seq });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/replay\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const fromNodeId = body?.fromNodeId != null ? String(body.fromNodeId) : \"\";\n const fromSeq = body?.fromSeq;\n if (fromNodeId) {\n sendJson(res, 200, meshReplayFromNodeId(roomUid, fromNodeId));\n return;\n }\n sendJson(res, 200, meshReplayFromSeq(roomUid, fromSeq));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/presence/mesh/subscribe\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const roomUid = String(q.searchParams.get(\"roomUid\") || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n const write = (chunk) => {\n try {\n res.write(chunk);\n } catch {\n /* closed */\n }\n };\n const unsub = meshSubscribeSse(roomUid, write);\n const onClose = () => {\n unsub();\n meshLeave(roomUid, actor.clientUid);\n try {\n res.end();\n } catch {\n /* ignore */\n }\n };\n req.on(\"close\", onClose);\n req.on(\"aborted\", onClose);\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, uid: auth.uid, mode: auth.mode, items: await listConnections(auth.uid) });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const id = await createConnection(auth.uid, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"create_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n await updateConnection(auth.uid, id, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"update_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"DELETE\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n await deleteConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"delete_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections/test\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n if (!checkHttpRateLimit(`llm_conn_test:${auth.uid}`, RL_LLM_CONN_TEST_PER_MIN, 60_000)) {\n return sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n }\n const hasBodyKey = !!String(payload?.apiKey || \"\").trim();\n const result = await queryRhizohLlm(\n {\n message: String(payload?.message || \"Ping from Castle Gateway\"),\n context: payload?.context || { source: \"connection_test\" },\n provider: payload?.provider,\n model: payload?.model,\n apiKey: payload?.apiKey || \"\"\n },\n { allowExternalApiKey: true, llmKeySource: hasBodyKey ? \"auto\" : \"env\" }\n );\n logLlmAccess({\n route: \"/llm/connections/test\",\n uid: auth.uid,\n llmKeyBillingOwner: result.llmKeyBillingOwner,\n llmKeyOrigin: result.llmKeyOrigin,\n provider: result.provider,\n model: result.model,\n connectionId: null\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"connection_test_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAgentIdentities(auth.uid);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerAgentIdentity({\n ownerUid: auth.uid,\n agentId: payload?.agentId,\n role: payload?.role,\n personaSeed: payload?.personaSeed || {},\n capabilityLevel: payload?.capabilityLevel\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/agents/identities/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const owns = await getAgentIdentity(id);\n if (!owns || owns.ownerUid !== auth.uid) return sendJson(res, 403, { ok: false, error: \"forbidden\" });\n const payload = await readHttpJson(req);\n const row = await updateAgentIdentity(id, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/academy/events\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/academy/events\"}`);\n const status = String(url.searchParams.get(\"status\") || \"\");\n const items = await listAcademyEvents({ ownerUid: auth.uid, status, limit: 120 });\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/academy/events\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const ev = await queueAcademyEvent({\n type: payload?.type,\n roomId: payload?.roomId,\n ownerUid: auth.uid,\n participants: payload?.participants || [],\n topic: payload?.topic || \"academy\",\n payload: payload?.payload || {}\n });\n sendJson(res, 200, { ok: true, event: ev });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && String(req.url || \"\").startsWith(\"/academy/events/\") && String(req.url || \"\").endsWith(\"/resolve\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const parts = String(req.url || \"\").split(\"/\").filter(Boolean);\n const eventId = parts[2] || \"\";\n try {\n const payload = await readHttpJson(req);\n const out = await resolveAcademyEvent(eventId, payload || {});\n sendJson(res, 200, { ok: true, event: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_resolve_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/memory\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/memory\"}`);\n const scope = url.searchParams.get(\"scope\") === \"agents\" ? \"agents\" : \"users\";\n const id = String(url.searchParams.get(\"id\") || (scope === \"agents\" ? \"\" : auth.uid));\n const kind = String(url.searchParams.get(\"kind\") || \"\");\n const limit = Number(url.searchParams.get(\"limit\") || 40);\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n sendJson(res, 200, { ok: true, scope, id, kind, items: await listMemories({ scope, id, kind, limit }) });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const profile = await getPersonaGoalMemory(auth.uid);\n sendJson(res, 200, { ok: true, profile });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const profile = await setPersonaGoalMemory({\n uid: auth.uid,\n personaPatch: payload?.personaPatch || {},\n goals: Array.isArray(payload?.goals) ? payload.goals : null,\n preferencesPatch: payload?.preferencesPatch || {}\n });\n sendJson(res, 200, { ok: true, profile });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"profile_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/compact\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const out = await autoCompactMemories({ scope, id });\n sendJson(res, 200, { ok: true, scope, id, ...out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_compact_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/context\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const context = await getMemoryContext({\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n query: String(payload?.query || \"\"),\n limit: Number(payload?.limit || 80)\n });\n sendJson(res, 200, { ok: true, context });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_context_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/place-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const placeName = String(payload?.placeName || \"\").trim();\n if (!placeName) return sendJson(res, 400, { ok: false, error: \"placeName_required\" });\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const persona = String(payload?.waypointPersona || payload?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(placeName)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Place: ${placeName}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Give a concise conversational briefing about this place for a walking-route companion.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-place-brief\", placeName, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `PLACE_BRIEF:${placeName} => ${String(summary.reply || \"\").slice(0, 900)}`,\n tags: [\"place\", \"brief\", \"event-layer\"],\n importance: 0.62,\n meta: { placeName }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"place-brief\",\n text: `${placeName} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: {\n placeName,\n directive: summary.directive,\n lat: hit?.lat ? Number(hit.lat) : null,\n lon: hit?.lon ? Number(hit.lon) : null\n }\n });\n sendJson(res, 200, { ok: true, place: hit || null, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"place_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/route-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const waypoints = Array.isArray(payload?.waypoints) ? payload.waypoints.slice(0, 24) : [];\n const out = [];\n for (const wp of waypoints) {\n const name = String(wp?.name || \"\").trim();\n if (!name) continue;\n const persona = String(wp?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(name)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Waypoint: ${name}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Generate a concise spoken briefing for route waypoint.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-route-brief\", waypoint: name, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n out.push({\n name,\n persona,\n place: hit || null,\n reply: summary.reply,\n directive: summary.directive\n });\n }\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `ROUTE_BRIEF: waypoints=${out.length} ${out.map((x) => `${x.name}:${x.persona}`).join(\" | \")}`.slice(0, 1300),\n tags: [\"route\", \"waypoint\", \"brief\", \"event-layer\"],\n importance: 0.74,\n meta: { count: out.length }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"route-brief\",\n text: out.map((x) => `${x.name}:${String(x.reply || \"\").slice(0, 140)}`).join(\" | \").slice(0, 1400),\n roomId: \"studio-main\",\n meta: {\n waypointCount: out.length,\n waypoints: out.map((x) => ({\n name: x.name,\n persona: x.persona,\n lat: x.place?.lat ? Number(x.place.lat) : null,\n lon: x.place?.lon ? Number(x.place.lon) : null\n }))\n }\n });\n sendJson(res, 200, { ok: true, items: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"route_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/pdf-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const title = String(payload?.title || \"document\");\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const providedText = String(payload?.text || \"\");\n const url = String(payload?.url || \"\");\n let content = providedText;\n if (!content && url) content = await fetchTextFromUrl(url);\n if (!content) return sendJson(res, 400, { ok: false, error: \"text_or_supported_url_required\" });\n const summary = await queryRhizohLlm(\n {\n message: `Summarize this document and provide key points + practical actions.\\nTitle:${title}\\nContent:\\n${content.slice(0, 12000)}`,\n context: { source: \"event-layer-pdf-brief\", title },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `DOC_BRIEF:${title} => ${String(summary.reply || \"\").slice(0, 1000)}`,\n tags: [\"document\", \"brief\", \"library\", \"event-layer\"],\n importance: 0.7,\n meta: { title, url: url || null }\n });\n await addTranscript(auth.uid, {\n source: \"library-brief\",\n eventType: \"pdf-brief\",\n text: `${title} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { title }\n });\n sendJson(res, 200, { ok: true, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/pdf/upload\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req, 8 * 1024 * 1024);\n const job = await enqueuePdfJob({\n uid: auth.uid,\n fileName: payload?.fileName || \"document.pdf\",\n bytesBase64: payload?.contentBase64\n });\n sendJson(res, 200, { ok: true, job });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_upload_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/session\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createPublishSession(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session\",\n text: `Publish session armed: ${row.protocol} ${row.target}`.slice(0, 500),\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: row.id, bridge: row.bridge }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/studio/publish/sessions\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listPublishSessions(auth.uid, 80);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/health-check\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const protocol = String(payload?.protocol || \"WHIP\");\n const target = String(payload?.target || \"\");\n const health = await checkPublishTargetHealth(protocol, target);\n if (payload?.sessionId) {\n await updatePublishSession(auth.uid, String(payload.sessionId), {\n status: health.ok ? \"healthy\" : \"degraded\",\n reconnectPolicy: {\n enabled: true,\n backoffSec: [2, 5, 10, 20],\n nextRetryInSec: health.reconnectInSec || 0,\n maxRetries: 8,\n reason: health.reason\n }\n });\n }\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-health\",\n text: `${protocol} ${target} => ${health.ok ? \"healthy\" : \"degraded\"} (${health.reason})`,\n roomId: \"studio-main\",\n meta: { protocol, target, reconnect: !!health.shouldReconnect }\n });\n sendJson(res, 200, { ok: true, health });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_health_check_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/studio/publish/sessions/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updatePublishSession(auth.uid, id, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session-update\",\n text: `Session ${id} -> ${row.status || \"updated\"}`,\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: id, status: row.status, ingestUrl: row.ingestUrl || \"\" }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/transcripts\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/transcripts\"}`);\n const limit = Math.max(1, Math.min(200, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await listTranscripts(auth.uid, limit);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/transcripts\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await addTranscript(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"transcript_write_failed\" });\n }\n return;\n }\n\n /** E1: broadcast intent → GreenRoom capability → transcript + memory artifact */\n if (req.method === \"POST\" && req.url === \"/studio/capabilities/greenroom\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const traceId = String(payload?.traceId || `TRC-${Date.now().toString(36).toUpperCase()}`);\n const title = String(payload?.title || \"Castle Live Broadcast\").slice(0, 200);\n const intentRaw = String(payload?.intentRaw || \"\").slice(0, 800);\n const audienceEstimate = Math.max(0, Math.min(50_000_000, Number(payload?.audienceEstimate) || 1200));\n const roomId = String(payload?.roomId || \"greenroom-main\");\n const transcriptRow = await addTranscript(auth.uid, {\n source: \"rhizoh\",\n eventType: \"BROADCAST_ROUTED\",\n text: `GreenRoom · ${title}${intentRaw ? `\\nIntent: ${intentRaw}` : \"\"}`,\n roomId,\n meta: {\n ack: \"BROADCAST_ROUTED\",\n capability: \"greenroom\",\n intentType: \"broadcast\",\n traceId,\n audienceEstimate,\n artifact: {\n kind: \"broadcast_card\",\n title,\n traceId,\n roomId,\n createdAt: Date.now()\n }\n }\n });\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n text: `GREENROOM_BROADCAST:${title} trace=${traceId}`,\n tags: [\"broadcast\", \"greenroom\", \"artifact\", \"castle-library\"],\n importance: 0.72,\n kind: \"episodic\",\n meta: { traceId, capability: \"greenroom\", transcriptId: transcriptRow.id }\n });\n sendJson(res, 200, {\n ok: true,\n ack: \"BROADCAST_ROUTED\",\n traceId,\n transcript: transcriptRow,\n replayPath: `/replay/${encodeURIComponent(traceId)}`,\n sharePath: `/replay/${encodeURIComponent(traceId)}`\n });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"greenroom_capability_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/metadata/search\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/metadata/search\"}`);\n const q = String(url.searchParams.get(\"q\") || \"\");\n const limit = Math.max(1, Math.min(300, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await searchMetadata(auth.uid, q, limit);\n sendJson(res, 200, { ok: true, q, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerDevice(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, device: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"device_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const lat = Number(payload?.lat);\n const lon = Number(payload?.lon);\n const speed = Math.max(0, Number(payload?.speed || 0));\n const cfgCenterLat = Number(process.env.CASTLE_GEOFENCE_CENTER_LAT || 41.0082);\n const cfgCenterLon = Number(process.env.CASTLE_GEOFENCE_CENTER_LON || 28.9784);\n const cfgRadiusM = Math.max(100, Number(process.env.CASTLE_GEOFENCE_RADIUS_M || 55000));\n const cfgSpeedCap = Math.max(1, Number(process.env.CASTLE_SPEED_CAP_MPS || 35));\n const dist = Number.isFinite(lat) && Number.isFinite(lon) ? haversineMeters(cfgCenterLat, cfgCenterLon, lat, lon) : 0;\n const geofenceOk = !Number.isFinite(dist) ? true : dist <= cfgRadiusM;\n const speedOk = speed <= cfgSpeedCap;\n const failsafe = !(geofenceOk && speedOk);\n const safety = {\n geofenceOk,\n speedOk,\n failsafe,\n distanceFromCenterM: Number.isFinite(dist) ? dist : null,\n speedCapMps: cfgSpeedCap\n };\n const row = await appendTelemetry(auth.uid, deviceId, {\n lat,\n lon,\n speed,\n battery: payload?.battery,\n mode: payload?.mode || (failsafe ? \"FAILSAFE_HOLD\" : \"ACTIVE\"),\n raw: payload?.raw || {}\n });\n await updateDevice(auth.uid, deviceId, {\n status: failsafe ? \"failsafe\" : \"active\",\n lastTelemetryAt: row.ts,\n safetyState: safety\n });\n if (failsafe) {\n await addTranscript(auth.uid, {\n source: \"robotics-guardrails\",\n eventType: \"failsafe\",\n text: `Device ${deviceId} failsafe triggered geofenceOk=${geofenceOk} speedOk=${speedOk}`,\n roomId: \"studio-main\",\n meta: { deviceId, safety }\n });\n }\n sendJson(res, 200, { ok: true, telemetry: row, safety, command: failsafe ? \"FAILSAFE_HOLD\" : \"CONTINUE\" });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"telemetry_ingest_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listTelemetry(auth.uid, 150);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createAutomation(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"automation-scheduled\",\n text: `Automation scheduled: ${row.title} at ${new Date(row.scheduleAt).toLocaleString()}`,\n roomId: \"studio-main\",\n meta: { automationId: row.id, action: row.action, deviceId: row.deviceId }\n });\n sendJson(res, 200, { ok: true, automation: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"automation_create_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAutomations(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listDevices(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const devs = await listDevices(auth.uid, 500);\n const d = devs.find((x) => x.id === deviceId);\n if (!d) return sendJson(res, 404, { ok: false, error: \"device_not_found\" });\n const action = String(payload?.action || \"\");\n const caps = Array.isArray(d.capabilityProfile) ? d.capabilityProfile : [];\n if (action && caps.length && !caps.includes(action) && !caps.includes(\"*\")) {\n return sendJson(res, 400, { ok: false, error: \"unsupported_action_for_device\", capabilityProfile: caps });\n }\n const cmd = await enqueueCommand(auth.uid, {\n deviceId,\n action: action || \"NOOP\",\n params: payload?.params || {},\n adapter: payload?.adapter || d.adapter || \"websocket\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-command-queue\",\n eventType: \"command-queued\",\n text: `${cmd.deviceId} -> ${cmd.action}`,\n roomId: \"studio-main\",\n meta: { commandId: cmd.id, adapter: cmd.adapter }\n });\n sendJson(res, 200, { ok: true, command: cmd });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listCommands(auth.uid, 200);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/robotics/commands/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updateCommandStatus(auth.uid, id, payload?.status || \"sent\");\n sendJson(res, 200, { ok: true, command: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/rhizoh/bridge\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const result = await runRhizohBrainV2({\n message: String(payload?.message || \"robotics bridge command\"),\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n worldState: payload?.worldState || {},\n deviceState: payload?.deviceState || {},\n intent: \"robotics-mechanics\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-bridge\",\n eventType: \"robotics-command\",\n text: `Bridge -> ${String(result.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { directive: result.directive, agentId: payload?.agentId || \"\" }\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"robotics_bridge_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/planner/graph\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const graph = buildTaskActionGraph(String(payload?.task || \"\"), payload?.context || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"planner-graph\",\n text: `Planner graph built: ${graph.taskTitle} nodes=${graph.nodes.length}`,\n roomId: \"studio-main\",\n meta: { taskTitle: graph.taskTitle, nodeCount: graph.nodes.length }\n });\n sendJson(res, 200, { ok: true, graph });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"planner_graph_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerSocialChannel(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, channel: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_channel_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listSocialChannels(auth.uid, 100);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/broadcast\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const text = String(payload?.text || \"\").slice(0, 1200);\n const channels = await listSocialChannels(auth.uid, 100);\n const enabled = channels.filter((c) => c.status === \"enabled\");\n const deliveries = [];\n for (const c of enabled) {\n const out = await deliverSocialWithRetry(c, text);\n deliveries.push({ id: c.id, platform: c.platform, ...out });\n }\n const okCount = deliveries.filter((d) => d.ok).length;\n await addTranscript(auth.uid, {\n source: \"social-layer\",\n eventType: \"social-broadcast\",\n text: `Broadcast delivered ${okCount}/${enabled.length} channel(s): ${text.slice(0, 240)}`,\n roomId: \"studio-main\",\n meta: { deliveries }\n });\n sendJson(res, 200, { ok: true, sentChannels: okCount, totalChannels: enabled.length, deliveries });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_broadcast_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/ethics/programs\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, ...buildEthicsPrograms() });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands/dispatch\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const all = await listCommands(auth.uid, 200);\n const queued = all.filter((c) => String(c.status || \"queued\") === \"queued\").slice(0, 50);\n const devices = await listDevices(auth.uid, 500);\n const byId = new Map(devices.map((d) => [d.id, d]));\n const results = [];\n for (const cmd of queued) {\n const dev = byId.get(cmd.deviceId);\n if (!dev) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: \"device_not_found\" });\n continue;\n }\n try {\n const dispatched = await dispatchCommandToAdapter(dev, cmd);\n await updateCommandStatus(auth.uid, cmd.id, \"sent\");\n results.push({ id: cmd.id, ok: true, adapter: dispatched.adapter, via: dispatched.via });\n } catch (error) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: String(error?.message || \"dispatch_failed\") });\n }\n }\n await addTranscript(auth.uid, {\n source: \"robotics-command-dispatch\",\n eventType: \"command-dispatch-batch\",\n text: `Dispatch batch size=${results.length} ok=${results.filter((x) => x.ok).length}`,\n roomId: \"studio-main\",\n meta: { results: results.slice(0, 30) }\n });\n sendJson(res, 200, { ok: true, processed: results.length, results });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"dispatch_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/pdf/jobs/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n const job = await getPdfJob(auth.uid, id);\n if (!job) return sendJson(res, 404, { ok: false, error: \"pdf_job_not_found\" });\n sendJson(res, 200, { ok: true, job });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const row = await appendMemory({\n scope,\n id,\n text: payload?.text,\n tags: payload?.tags || [],\n importance: payload?.importance,\n kind: payload?.kind || \"episodic\",\n meta: { uid: auth.uid, ...(payload?.meta || {}) }\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.externalTruth) {\n try {\n if (process.env.RHIZOH_EXTERNAL_TRUTH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_truth:${ip}`, RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n sendJson(res, 200, buildRhizohExternalGroundTruthPayload(req));\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.productOutcome) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const rawBody = await readHttpBodyText(req, 64 * 1024);\n const body = rawBody.trim() ? safeJsonParse(rawBody) : null;\n if (!body || typeof body !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"invalid_json_payload\" });\n return;\n }\n const sigHdr = String(req.headers?.[\"x-rhizoh-outcome-signature\"] || \"\").trim();\n const hmacV = verifyRhizohOutcomeHmac(rawBody, sigHdr);\n if (!hmacV.ok) {\n sendJson(res, 401, { ok: false, error: hmacV.error || \"outcome_hmac_invalid\" });\n return;\n }\n const srcTokHdr = String(req.headers?.[\"x-rhizoh-outcome-source-token\"] || \"\").trim();\n const srcV = verifyRhizohOutcomeSourceToken(body.source, srcTokHdr);\n if (!srcV.ok) {\n sendJson(res, 401, { ok: false, error: srcV.error || \"outcome_source_token_invalid\" });\n return;\n }\n const subjectKey = String(body.subjectKey || \"\").trim().slice(0, 128);\n if (\n subjectKey &&\n !checkHttpRateLimit(\n `rhizoh_product_outcome_subj:${subjectKey}`,\n RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN,\n 60_000\n )\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_subject\" });\n return;\n }\n const result = ingestRhizohProductOutcomeHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n schemaVersion: \"1.1.0\",\n aggregate: result.aggregate,\n integrity: result.integrity\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"outcome_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.productOutcomeAggregate) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome_agg:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const u = new URL(String(req.url || \"/\"), \"http://localhost\");\n const cohortId = u.searchParams.get(\"cohortId\") || \"\";\n const decisionFingerprint = u.searchParams.get(\"decisionFingerprint\") || \"\";\n const limit = u.searchParams.get(\"limit\") || \"50\";\n const rows = listRhizohProductOutcomeAggregates({\n cohortId: cohortId || undefined,\n decisionFingerprint: decisionFingerprint || undefined,\n limit: Number(limit) || 50\n });\n sendJson(res, 200, { ok: true, schemaVersion: \"1.1.0\", rows });\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.externalLossBatch) {\n try {\n if (process.env.RHIZOH_EXTERNAL_LOSS_BATCH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_loss_batch:${ip}`, RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const result = ingestRhizohExternalLossBatchHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, { ok: true, accepted: result.accepted ?? 0 });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"batch_failed\") });\n }\n return;\n }\n\n function moderationAdminOk(req) {\n const expected = String(process.env.CASTLE_MODERATION_ADMIN_KEY || \"\").trim();\n if (!expected) return false;\n const got = String(req.headers[\"x-castle-moderation-key\"] || \"\").trim();\n return got.length > 0 && got === expected;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.opsAbuseReport) {\n try {\n const body = await readHttpJson(req, 32 * 1024);\n const auth = await resolveHttpUser(req);\n const entry = submitAbuseReportV0({\n reporterUid: auth.ok ? auth.uid : null,\n targetUid: body?.targetUid ?? null,\n category: body?.category,\n detail: body?.detail,\n traceId: body?.traceId ?? null\n });\n sendJson(res, 202, { ok: true, reportId: entry.id, status: entry.status });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsModerationQueue) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n queue: listModerationQueueV0(100),\n softBlocked: listSoftBlockedUsersV0()\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-block\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const out = softBlockUserV0(body?.uid, body?.reason);\n sendJson(res, out.ok ? 200 : 400, out);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-unblock\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n sendJson(res, 200, removeSoftBlockV0(body?.uid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsAgentSnapshots) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n const limit = Number(new URL(req.url || \"\", \"http://local\").searchParams.get(\"limit\") || 50);\n sendJson(res, 200, { ok: true, snapshots: listRecentAgentSnapshotsV0(limit) });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.academicObservatoryExport) {\n const auth = await resolveHttpUser(req);\n const out = handleAcademicObservatoryExportGetV0(req, auth);\n sendJson(res, out.status, out.body);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsHardeningStatus) {\n const auth = await resolveHttpUser(req);\n const principal = auth.ok ? `uid:${auth.uid}` : null;\n const gclSnap = await getGlobalCostLedgerSnapshotV0(principal);\n const economicStrategy = evaluateEconomicStrategyL3V0(gclSnap);\n const hardeningTenantId = resolveHardeningTenantIdV0(auth, req);\n const unifiedState = await buildUnifiedStateNarrativeV0({\n tenantId: hardeningTenantId ?? undefined,\n platformScope: !hardeningTenantId,\n principal\n });\n sendJson(res, 200, {\n ok: true,\n schema: \"rhizoh.ops.hardening_status.v0\",\n containment: readAgentContainmentConfigV0(),\n cost: readCostContainmentConfigV0(),\n gcl: {\n backend: await getGclBackendStatusV0(),\n snapshot: gclSnap\n },\n economicStrategyL3: economicStrategy,\n phasedRollout: await getPhasedRolloutStatsV0(),\n tier: readPhasedRolloutTierV0(),\n principalCost: auth.ok ? await getCostContainmentStatsV0(principal) : null,\n stressTaxonomy: verifyStressTaxonomyCoverageV0(),\n stressConflictResolution: verifyStressConflictResolutionV0(),\n resolutionStabilityEnvelope: verifyResolutionStabilityEnvelopeV0(),\n unifiedState: {\n tenantScope: {\n tenantId: unifiedState.tenantScope?.tenantId,\n scope: unifiedState.tenantScope?.scope,\n noGlobalDerivedState: unifiedState.tenantScope?.noGlobalDerivedState\n },\n narrativeFingerprint: unifiedState.narrativeFingerprint,\n screenshotScopeWatermark: unifiedState.screenshotScopeWatermark,\n interpretationSafetyContract: unifiedState.interpretationSafetyContract,\n interpretationUxContract: {\n schema: unifiedState.interpretationUxContract?.schema,\n layerOrder: unifiedState.interpretationUxContract?.layerOrder,\n narrativeNeverVisuallyDominant: unifiedState.interpretationUxContract?.narrativeNeverVisuallyDominant\n },\n governance: {\n narrativeIsNotDecisionLayer: unifiedState.governance.narrativeIsNotDecisionLayer,\n displayOrder: unifiedState.governance.displayOrder,\n trustPosture: unifiedState.governance.trustPosture,\n interpretationUxContract: unifiedState.governance.interpretationUxContract?.schema\n },\n humanOps: {\n decisionTier: unifiedState.humanOps?.humanDecisionScaling?.tier,\n decisionOwner: unifiedState.humanOps?.humanDecisionScaling?.decisionOwner,\n opsRouteId: unifiedState.humanOps?.humanDecisionOpsRunbook?.routing?.routeId,\n slaAckMinutes: unifiedState.humanOps?.humanDecisionOpsRunbook?.sla?.ackMinutes,\n uiDisclaimerRequired: unifiedState.humanOps?.narrativeUiSafety?.displayRules?.requireDisclaimerBanner,\n misreadHighResidual: unifiedState.humanOps?.misreadSimulation?.highResidualCount,\n propagationHighResidual: unifiedState.humanOps?.socialPropagationSimulation?.highResidualCount,\n worstWatermarkSurvivability:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.worstWatermarkSurvivability,\n dominantDistortionSource:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.dominantDistortionSource\n },\n culturalRisk: {\n perceptualRiskClass: unifiedState.culturalRisk?.perceptualRiskClass,\n trustFork: unifiedState.culturalRisk?.trustDynamics?.fork,\n trustDecayScore: unifiedState.culturalRisk?.trustDynamics?.scores?.trustDecay,\n mythologyScore: unifiedState.culturalRisk?.trustDynamics?.scores?.mythology,\n confidenceDetachmentExposure:\n unifiedState.culturalRisk?.trustDynamics?.axes?.confidenceDetachmentExposure\n },\n appliedSystemsLayer: {\n schema: unifiedState.appliedSystemsLayer?.schema,\n researchOutputType: unifiedState.appliedSystemsLayer?.researchGovernance?.outputType,\n roboticsBlockActuation: unifiedState.appliedSystemsLayer?.roboticsGrounding?.blockActuation,\n spiralRumorCount:\n unifiedState.appliedSystemsLayer?.spiralMMOGameKernel?.gameLayer?.rumors?.length\n },\n actionSemanticGovernance: {\n schema: unifiedState.actionSemanticGovernance?.schema,\n highRiskActionCount: unifiedState.actionSemanticGovernance?.highRiskActionCount,\n loopClosed: unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.loopClosed,\n authorityCollapseMitigated:\n unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.risks?.authorityCollapse\n ?.mitigated\n },\n actionContextResolution: {\n schema: unifiedState.actionContextResolution?.schema,\n contextFingerprint: unifiedState.actionContextResolution?.contextFingerprint?.fingerprintHash,\n domainCollision: unifiedState.actionContextResolution?.domainCollision?.detected,\n deployAgentSpiralBlocked:\n unifiedState.actionContextResolution?.actionOverloadingRisk?.deployAgentExample\n ?.spiral_mmo === \"blocked\",\n roboticsActuationEligible:\n unifiedState.actionContextResolution?.roboticsActuationCorrectness?.actuationEligible\n },\n epistemicCoherence: {\n schema: unifiedState.epistemicCoherence?.schema,\n verdict: unifiedState.epistemicCoherence?.systemCoherence?.verdict,\n coherenceScore: unifiedState.epistemicCoherence?.systemCoherence?.score,\n contradictionCount: unifiedState.epistemicCoherence?.crossLayerContradictions?.count,\n operatorDecision: unifiedState.epistemicCoherence?.uxCompression?.operatorDecision,\n headlineTr: unifiedState.epistemicCoherence?.uxCompression?.headline?.tr,\n falseSafetyWarning: Boolean(\n unifiedState.epistemicCoherence?.uxCompression?.falseSafetyWarning\n )\n },\n coherenceAuthorityBoundary: {\n schema: unifiedState.coherenceAuthorityBoundary?.schema,\n bindingValid: unifiedState.coherenceAuthorityBoundary?.eclAuthorityBinding?.valid,\n canExecute: unifiedState.coherenceAuthorityBoundary?.contract?.can_execute,\n mandatoryBannerTr:\n unifiedState.coherenceAuthorityBoundary?.disclaimers?.mandatoryBanner?.tr\n },\n decisionLatencyGovernance: {\n schema: unifiedState.decisionLatencyGovernance?.schema,\n latencyTier: unifiedState.decisionLatencyGovernance?.latencyTier,\n inflationScore: unifiedState.decisionLatencyGovernance?.latencyInflation?.score,\n fastPathEligible: unifiedState.decisionLatencyGovernance?.fastPath?.eligible,\n primaryActionId: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.primaryActionId,\n ackSlaMinutes: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.ackSlaMinutes,\n certaintyCap: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.certaintyCap,\n uncertaintyItems:\n unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.uncertaintyEnvelope?.itemCount\n },\n decisionPacketUncertaintyBoundary: {\n schema: unifiedState.decisionPacketUncertaintyBoundary?.schema,\n invariantValid: unifiedState.decisionPacketUncertaintyBoundary?.invariantCheck?.valid,\n centralInvariant: unifiedState.decisionPacketUncertaintyBoundary?.centralInvariant\n },\n epistemicDecisionPacing: {\n schema: unifiedState.epistemicDecisionPacing?.schema,\n queueSaturationTier:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation?.tier,\n operatorProcessingLatency:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation\n ?.operatorProcessingLatency,\n postureWindowCount:\n unifiedState.epistemicDecisionPacing?.temporaryStaticPostureWindows\n ?.temporaryStaticPostureWindows?.windowCount,\n executionBoundariesValid:\n unifiedState.epistemicDecisionPacing?.executionBoundaries?.valid,\n grantsExecutionApproval: unifiedState.epistemicDecisionPacing?.grantsExecutionApproval\n },\n epistemicTemporalCoherence: {\n schema: unifiedState.epistemicTemporalCoherence?.schema,\n allInvariantsValid: unifiedState.epistemicTemporalCoherence?.allInvariantsValid,\n temporalAlignmentValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.temporalAlignment?.valid,\n crossWindowGuardValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard?.valid,\n delayedTruthValid: unifiedState.epistemicTemporalCoherence?.invariants?.delayedTruthRule?.valid,\n reconciliationRequired:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard\n ?.reconciliation?.required\n },\n realityDriftObserver: {\n schema: unifiedState.realityDriftObserver?.schema,\n driftActive: unifiedState.realityDriftObserver?.driftObserverSummary?.active,\n misapprehensionShapeCount:\n unifiedState.realityDriftObserver?.misapprehensionShapeCatalog?.shapeCount,\n criticalShapeCount:\n unifiedState.realityDriftObserver?.driftObserverSummary?.criticalShapeCount,\n roboticsMismatchActive:\n unifiedState.realityDriftObserver?.roboticsFeedbackMismatch?.active\n },\n driftCausality: {\n schema: unifiedState.driftCausality?.schema,\n shapesWithCausalExplanation: unifiedState.driftCausality?.rdolLinkage?.shapesWithCausalExplanation,\n activeDomainCount: unifiedState.driftCausality?.causalitySummary?.activeDomainCount,\n propagationCausalityActive: unifiedState.driftCausality?.causalDomains?.propagation?.active\n },\n productReadiness: unifiedState.productReadiness,\n stateLayers: {\n raw: unifiedState.stateLayers.raw,\n derived: {\n systemState: unifiedState.stateLayers.derived.systemState,\n narrative: unifiedState.stateLayers.derived.narrative,\n validation: unifiedState.stateLayers.derived.validation\n },\n policy: {\n binding: unifiedState.stateLayers.policy.binding,\n decisionOwner: unifiedState.stateLayers.policy.decisionOwner,\n prohibitedActions: unifiedState.stateLayers.policy.prohibitedActions\n }\n },\n systemState: unifiedState.systemState,\n interpretation: unifiedState.interpretation,\n validation: {\n trustPosture: unifiedState.validation.trustPosture,\n adjustedConfidence: unifiedState.validation.adjustedConfidence,\n agreement: unifiedState.validation.confidenceDecomposition.agreement,\n divergenceScore: unifiedState.validation.divergence.divergenceScore,\n validated: unifiedState.validation.divergence.validated,\n uncertaintyTags: unifiedState.validation.uncertainty.tags.map((t) => t.id),\n flags: unifiedState.validation.divergence.flags.map((f) => f.id)\n },\n compression: unifiedState.compression\n }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3) {\n try {\n const payload = await readHttpJson(req, 6 * 1024 * 1024);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (\n !checkHttpRateLimit(`rhizoh_voice_transcribe:${rlKey}`, RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN, 60_000)\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const voiceEnv = rhizohVoiceTranscribeEnvV3();\n if (!voiceEnv.any) {\n sendJson(res, 503, {\n ok: false,\n error: \"voice_asr_not_configured\",\n env: voiceEnv\n });\n return;\n }\n const result = await runRhizohVoiceTranscribeV3(payload);\n sendJson(res, result.ok ? 200 : 422, {\n ...result,\n traceId: String(payload?.traceId || \"\"),\n atMs: Date.now()\n });\n } catch (error) {\n sendJson(res, 500, {\n ok: false,\n error: \"voice_transcribe_failed\",\n detail: String(error?.message || error)\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === rhizohRuntime.routes.rhizohLlm) {\n try {\n const payload = await readHttpJson(req);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n logRhizohHealth(\"gateway_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: String(payload?.traceId || \"\"),\n auth: auth.ok ? \"ok\" : \"anon\",\n ip\n });\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (!checkHttpRateLimit(`rhizoh_llm:${rlKey}`, RL_RHIZOH_LLM_PER_MIN, 60_000)) {\n const rlTaxonomy = classifyStressResponseV0({ code: \"rate_limit_exceeded\" });\n return sendJson(res, 429, {\n ok: false,\n error: \"rate_limit_exceeded\",\n reply: rlTaxonomy.userMessageTr || \"İstek sınırı aşıldı. Kısa süre sonra tekrar deneyin.\",\n stressClass: rlTaxonomy.stressClass,\n responseAction: rlTaxonomy.responseAction,\n stressMatrix: rlTaxonomy.matrix,\n stressInterpretable: rlTaxonomy.interpretable,\n stressConfidence: rlTaxonomy.stressConfidence,\n actionConfidence: rlTaxonomy.actionConfidence,\n actionSoftened: rlTaxonomy.actionSoftened,\n responseActionStrict: rlTaxonomy.responseActionStrict,\n actionInterpretable: rlTaxonomy.actionInterpretable,\n conflictResolution: rlTaxonomy.conflictResolution,\n directive: \"NONE\"\n });\n }\n\n const requireExplicit = process.env.CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE === \"1\";\n const keyMode = normalizeClientLlmKeySource(payload?.llmKeySource ?? payload?.keySource);\n if (keyMode === null) {\n return sendJson(res, 400, {\n ok: false,\n error: \"invalid_llm_key_source\",\n reply: \"llmKeySource geçersiz. Kullanın: env | user_connection | auto\",\n directive: \"NONE\"\n });\n }\n if (requireExplicit && keyMode === \"auto\") {\n return sendJson(res, 400, {\n ok: false,\n error: \"llm_key_source_required\",\n reply: \"Sunucu CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE=1: istekte llmKeySource (env veya user_connection) zorunlu.\",\n directive: \"NONE\"\n });\n }\n\n const conn = auth.ok ? await resolveConnection(auth.uid, String(payload?.connectionId || \"\")) : null;\n\n if (keyMode === \"user_connection\") {\n if (!auth.ok) {\n return sendJson(res, 401, {\n ok: false,\n error: \"llm_auth_required_for_user_connection\",\n reply: \"Kayıtlı LLM bağlantısı için giriş gerekli.\",\n directive: \"NONE\"\n });\n }\n if (!conn?.apiKey) {\n return sendJson(res, 400, {\n ok: false,\n error: \"user_llm_connection_required\",\n reply: \"Geçerli bir LLM bağlantısı yok; Studio’da bağlantı ekleyin veya llmKeySource=env kullanın.\",\n directive: \"NONE\"\n });\n }\n }\n\n const safePayload = { ...(payload || {}) };\n delete safePayload.apiKey;\n delete safePayload.llmKeySource;\n delete safePayload.keySource;\n\n let resolvedProvider = payload?.provider;\n let resolvedModel = payload?.model;\n let connApiKey = \"\";\n\n if (keyMode === \"user_connection\") {\n resolvedProvider = conn.provider;\n resolvedModel = conn.model;\n connApiKey = conn.apiKey || \"\";\n } else if (keyMode === \"env\") {\n connApiKey = \"\";\n } else {\n resolvedProvider = conn?.provider || payload?.provider;\n resolvedModel = conn?.model || payload?.model;\n connApiKey = conn?.apiKey || \"\";\n }\n\n const { result, traceId, turnLatencyMs, spinePhases, sampledTrace } = await rhizohGatewayTurn({\n safePayload,\n auth,\n keyMode,\n conn,\n resolvedProvider,\n resolvedModel,\n connApiKey\n });\n logRhizohHealth(\"llm_response\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId,\n turnLatencyMs: Number(turnLatencyMs || 0),\n keyMode\n });\n sendJson(res, 200, {\n ...result,\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n traceId,\n turnLatencyMs,\n ...(spinePhases ? { spinePhases } : {}),\n ...(sampledTrace !== undefined ? { sampledTrace } : {})\n });\n } catch (error) {\n const msg = String(error?.message || \"\");\n const code = error?.code || \"\";\n let status = 500;\n if (msg === \"rate_limit_exceeded\") status = 429;\n else if (\n code === \"cost_hard_limit\" ||\n code === \"phased_rollout_capacity\" ||\n error?.containment === true ||\n String(code).startsWith(\"agent_\")\n ) {\n status = 429;\n } else if (code === \"user_soft_blocked\" || code === \"prompt_abuse_detected\") status = 403;\n else if ([\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(code) || msg.includes(\"missing_api_key_for_\"))\n status = 400;\n let rhizohFailureKind = \"provider_error\";\n if (msg === \"rate_limit_exceeded\" || status === 429) rhizohFailureKind = \"rate_limit\";\n else if (status === 403) rhizohFailureKind = \"policy_block\";\n else if (status === 400) rhizohFailureKind = \"client_config\";\n else if (msg.startsWith(\"provider_http_\")) {\n const st = Number(msg.replace(\"provider_http_\", \"\"));\n if (st === 408 || st === 504) rhizohFailureKind = \"timeout\";\n else rhizohFailureKind = \"provider_error\";\n } else if (/timeout|timed out|ETIMEDOUT/i.test(msg)) rhizohFailureKind = \"timeout\";\n\n const providerHttpMatch = msg.match(/^provider_http_(\\d{3})$/);\n const stressTaxonomy =\n error?.stressClass != null\n ? {\n stressClass: error.stressClass,\n responseAction: error.responseAction,\n stressMatrix: error.stressMatrix,\n stressInterpretable: error.stressInterpretable !== false,\n stressConfidence: error.stressConfidence,\n actionConfidence: error.actionConfidence,\n actionSoftened: error.actionSoftened,\n responseActionStrict: error.responseActionStrict,\n actionInterpretable: error.actionInterpretable,\n conflictResolution: error.conflictResolution,\n stressSecondary: error.stressSecondary,\n responseActions: error.responseActions\n }\n : (() => {\n const t = classifyStressResponseV0({\n code: code || msg,\n rhizohFailureKind,\n providerHttpStatus: providerHttpMatch\n ? Number(providerHttpMatch[1])\n : null\n });\n return {\n stressClass: t.stressClass,\n responseAction: t.responseAction,\n stressMatrix: t.matrix,\n stressInterpretable: t.interpretable,\n stressConfidence: t.stressConfidence,\n actionConfidence: t.actionConfidence,\n actionSoftened: t.actionSoftened,\n responseActionStrict: t.responseActionStrict,\n actionInterpretable: t.actionInterpretable,\n conflictResolution: t.conflictResolution,\n stressSecondary: t.stressSecondary,\n responseActions: t.responseActions\n };\n })();\n sendJson(res, status, {\n ok: false,\n rhizohFailureKind,\n ...(providerHttpMatch ? { providerHttpStatus: Number(providerHttpMatch[1]) } : {}),\n error: code || msg || \"rhizoh_llm_failed\",\n reply:\n error?.reply ||\n (status === 500 ? \"Rhizoh bağlantısı geçici olarak kesildi.\" : msg),\n directive: error?.directive || \"NONE\",\n ...stressTaxonomy\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicSeal) {\n try {\n if (!EPISTEMIC_SEAL_SECRET) {\n sendJson(res, 503, { ok: false, error: \"epistemic_seal_disabled\" });\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`epistemic_seal:${ip}`, RL_EPISTEMIC_SEAL_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const truth = body.truth_contract;\n if (!truth || typeof truth !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"truth_contract_required\" });\n return;\n }\n const sealBody = {\n truth_contract: truth,\n runtime_hash: String(body.runtime_hash || \"\"),\n model_route: {\n provider: body.model_route?.provider ?? null,\n model: body.model_route?.model ?? null\n },\n memory_digest: String(body.memory_digest || \"\"),\n world_snapshot_hash: String(body.world_snapshot_hash || \"\"),\n timestamp: Number(body.timestamp) || Date.now()\n };\n const canonical = canonicalEpistemicSealString(sealBody);\n const { hash, signature } = hashAndSignEpistemicSeal(canonical, EPISTEMIC_SEAL_SECRET);\n recordGenesisEpistemicSealIssued(hash);\n const attestation = {\n schema: EPISTEMIC_SEAL_SCHEMA,\n algorithm: \"SHA-256+HMAC-SHA256\",\n keyHint: process.env.CASTLE_EPISTEMIC_SEAL_SECRET ? \"CASTLE_EPISTEMIC_SEAL_SECRET\" : \"CASTLE_GATEWAY_TOKEN\",\n issuedAt: Date.now(),\n gateway: { service: \"castle-gateway\", listenPort: PORT }\n };\n sendJson(res, 200, {\n ok: true,\n hash,\n signature,\n attestation,\n canonicalBytes: Buffer.byteLength(canonical, \"utf8\")\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"seal_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicLogsBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`epistemic_logs:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 120) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const persisted = await persistEpistemicLedgerBatch(auth.uid, entries);\n recordGenesisEpistemicLedgerPersisted(persisted.persisted);\n // Internal cognition layer: forecast stays gateway/ledger-only (no client exposure).\n try {\n await persistEpistemicForecastBatch(auth.uid, persisted.normalized || []);\n } catch {\n /* forecast failures must not break primary ledger ingest */\n }\n sendJson(res, 200, {\n ok: true,\n persisted: persisted.persisted,\n mode: persisted.mode,\n latest: persisted.normalized?.[persisted.normalized.length - 1] || null\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"epistemic_log_ingest_failed\") });\n }\n return;\n }\n\n sendJson(res, 404, { ok: false, error: \"not_found\" });\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\nspiralWssForGenesis = wss;\nconst orchestrator = createOrchestrator();\nconst clientStats = new Map();\nlet broadcasterClientId = null;\nconst spiralState = {\n rooms: [{ id: \"room-default\", name: \"Spiral Default Arena\" }],\n activeByClient: new Map(),\n characters: new Map() // id -> {id,name,role,roomId,source}\n};\n\n/** Castle social rooms — keyed by `castleRoomKey` from {@link WS_MESSAGE.CASTLE_SOCIAL_PULSE}. */\nconst castleSocialRoomState = new Map();\nconst CASTLE_SOCIAL_MEMBER_TTL_MS = 90_000;\nconst CASTLE_SOCIAL_MAX_PULSE_BYTES = 8192;\n\nfunction removeCastleSocialClientFromAllRooms(clientId) {\n const cid = String(clientId || \"\");\n if (!cid) return;\n for (const room of castleSocialRoomState.values()) {\n room.members.delete(cid);\n }\n}\n\nfunction buildCastleSocialRoomPayload(roomKey) {\n const room = castleSocialRoomState.get(roomKey);\n if (!room) return null;\n const now = Date.now();\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const roster = [...room.members.values()].map((m) => ({\n userId: m.userId,\n lastMs: m.lastMs,\n ...m.sp\n }));\n const peerFeeds =\n room.walPeers && room.walPeers.size\n ? [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }))\n : [];\n return { castleRoomKey: roomKey, seq: room.seq, roster, peerFeeds, ts: now };\n}\n\nconst CASTLE_WAL_PEER_MAX_FEED_BYTES = 48 * 1024;\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {{ ok: boolean, code?: string, reason?: string }} gate\n */\nfunction rejectWalPeerIngressV0(socket, gate) {\n const code = gate.code || \"wal_peer_rejected\";\n recordWalPeerRejectV0(code);\n recordSubstrateOtelEventV0(\"wal_peer_reject\", { code, reason: String(gate.reason || \"\").slice(0, 120) });\n socket.send(\n JSON.stringify(\n createEnvelope(WS_MESSAGE.ERROR, {\n error: gate.reason || \"WAL peer ingress rejected.\",\n code\n })\n )\n );\n}\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {Record<string, unknown> | null | undefined} walPeerFeed\n * @returns {boolean}\n */\nfunction gateWalPeerIngressV0(socket, walPeerFeed) {\n const authGate = assertWalPeerSocketAuthorityV0(socket);\n if (!authGate.ok) {\n rejectWalPeerIngressV0(socket, authGate);\n return false;\n }\n const sigGate = validateWalPeerFeedSignatureV0(\n walPeerFeed && typeof walPeerFeed === \"object\" ? walPeerFeed : {}\n );\n if (!sigGate.ok) {\n rejectWalPeerIngressV0(socket, sigGate);\n return false;\n }\n return true;\n}\n\nfunction handleCastleWalPeerFeed(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_WAL_PEER_MAX_FEED_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_WAL_PEER_FEED too large.\" })));\n return;\n }\n const walPeerFeed = p.walPeerFeed && typeof p.walPeerFeed === \"object\" ? p.walPeerFeed : {};\n if (!gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const castleRoomKey = String(p.castleRoomKey || \"wal:convergence\").slice(0, 64);\n const castleId = String(p.castleId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId,\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: room.members.get(socket.clientId)?.sp ?? {}\n });\n const peerFeeds = [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }));\n const body = {\n castleRoomKey,\n seq: room.seq,\n peerFeeds,\n ts: now\n };\n const env = createEnvelope(WS_MESSAGE.CASTLE_WAL_PEER_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys(), ...room.walPeers.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction handleCastleSocialPulse(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_SOCIAL_MAX_PULSE_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_SOCIAL_PULSE too large.\" })));\n return;\n }\n const castleRoomKey = String(p.castleRoomKey || \"default\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n const sp = p.socialPulse && typeof p.socialPulse === \"object\" ? p.socialPulse : {};\n const walPeerFeed =\n p.walPeerFeed && typeof p.walPeerFeed === \"object\"\n ? p.walPeerFeed\n : sp.walPeerFeed && typeof sp.walPeerFeed === \"object\"\n ? sp.walPeerFeed\n : null;\n if (walPeerFeed && !gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const spNext = walPeerFeed ? { ...sp, walPeerFeed } : { ...sp };\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: spNext\n });\n if (walPeerFeed) {\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId: String(p.castleId || userId).slice(0, 64),\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n }\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const body = buildCastleSocialRoomPayload(castleRoomKey);\n if (!body) return;\n const env = createEnvelope(WS_MESSAGE.CASTLE_SOCIAL_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcast(msgObj) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcastState() {\n broadcast(\n createEnvelope(WS_MESSAGE.BROADCAST_STATE, {\n broadcasterClientId,\n viewers: [...wss.clients].filter((c) => c.readyState === 1).length\n })\n );\n}\n\nfunction currentSpiralStatePayload() {\n return {\n rooms: spiralState.rooms,\n characters: [...spiralState.characters.values()]\n };\n}\n\nfunction broadcastSpiralState() {\n broadcast(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload()));\n}\n\nfunction ensureRoom(roomId, fallbackName = null) {\n if (!roomId) return spiralState.rooms[0].id;\n const exists = spiralState.rooms.find((r) => r.id === roomId);\n if (exists) return exists.id;\n const created = { id: roomId, name: fallbackName || `Spiral-${roomId.slice(-4)}` };\n spiralState.rooms.push(created);\n return created.id;\n}\n\nfunction syncSpiralCharactersWithWorld(snapshot) {\n const agents = snapshot?.agents || [];\n for (const a of agents) {\n if (!spiralState.characters.has(a.id)) {\n spiralState.characters.set(a.id, {\n id: a.id,\n name: a.id,\n role: \"agent\",\n roomId: spiralState.rooms[0].id,\n source: \"world\"\n });\n }\n }\n}\n\nfunction broadcastExcept(msgObj, exceptSocket) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client !== exceptSocket && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction validateInputFrame(payload, socket) {\n const stats = clientStats.get(socket.clientId) || { lastFrame: 0, windowStart: Date.now(), msgCount: 0 };\n const now = Date.now();\n if (now - stats.windowStart > 5000) {\n stats.windowStart = now;\n stats.msgCount = 0;\n }\n stats.msgCount += 1;\n clientStats.set(socket.clientId, stats);\n if (stats.msgCount > 120) return \"Rate limit exceeded.\";\n\n if (!payload || !Number.isInteger(payload.frame)) return \"frame must be integer.\";\n if (payload.frame <= stats.lastFrame) return \"frame must be monotonic.\";\n const commands = Array.isArray(payload.commands) ? payload.commands : [];\n if (commands.length > 8) return \"Too many commands in one frame.\";\n for (const cmd of commands) {\n if (!cmd || typeof cmd.command !== \"string\") return \"Invalid command format.\";\n if (\n cmd.command !== \"SPAWN_AGENT\" &&\n cmd.command !== \"LIST_CASTLES\" &&\n cmd.command !== \"SPAWN_ENTITY\" &&\n cmd.command !== \"CREATE_CASTLE\"\n ) return \"Unsupported command.\";\n }\n stats.lastFrame = payload.frame;\n clientStats.set(socket.clientId, stats);\n return null;\n}\n\nfunction validateSignalPayload(payload) {\n if (!payload || typeof payload !== \"object\") return \"Invalid signaling payload.\";\n const signalType = payload.signalType;\n if (![\"OFFER\", \"ANSWER\", \"ICE\"].includes(signalType)) return \"Invalid signal type.\";\n if (typeof payload.to !== \"string\" || payload.to.length < 3) return \"Invalid signal destination.\";\n if (signalType !== \"ICE\" && JSON.stringify(payload.sdp || {}).length > 16 * 1024) return \"SDP too large.\";\n if (signalType === \"ICE\" && JSON.stringify(payload.candidate || {}).length > 6 * 1024) return \"ICE too large.\";\n return null;\n}\n\nfunction sanitizeRawMessage(raw) {\n if (!raw) return null;\n const text = raw.toString();\n if (Buffer.byteLength(text, \"utf8\") > MAX_MESSAGE_BYTES) return null;\n return text;\n}\n\nwss.on(\"connection\", async (socket, req) => {\n const origin = req?.headers?.origin || \"\";\n if (!httpCors.isWebSocketOriginAllowed(origin)) {\n socket.close(1008, \"Origin not allowed\");\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const path = req?.url || \"\";\n const query = new URL(`ws://localhost${path}`).searchParams;\n const token = query.get(\"token\");\n if (!token || token !== REQUIRED_GATEWAY_TOKEN) {\n socket.close(1008, \"Invalid gateway token\");\n return;\n }\n }\n if (REQUIRE_AUTH) {\n const result = await verifyClientToken(req);\n if (!result.ok) {\n const isLocal = origin.includes(\"localhost\") || origin.includes(\"127.0.0.1\") || !origin;\n if (!(ALLOW_DEV_ANON && isLocal)) {\n socket.close(1008, result.reason);\n return;\n }\n socket.auth = { ok: true, kind: \"dev-anon\", user: { uid: \"dev-anon\" } };\n } else {\n socket.auth = result;\n }\n }\n\n socket.clientId = `c-${Math.random().toString(36).slice(2, 8)}`;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.HELLO, { message: \"Castle Gateway online.\", clientId: socket.clientId })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.WORLD_SNAPSHOT, orchestrator.worldSnapshot())));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.BROADCAST_STATE, { broadcasterClientId, viewers: [...wss.clients].filter((c) => c.readyState === 1).length })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload())));\n broadcastExcept(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }), socket);\n\n socket.on(\"message\", (raw) => {\n const safeRaw = sanitizeRawMessage(raw);\n if (!safeRaw) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Message too large or invalid.\" })));\n return;\n }\n const parsed = safeJsonParse(safeRaw);\n if (!parsed?.type) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid message.\" })));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND_TEXT) {\n const canonical = parseTextToCommand(parsed.payload?.text);\n if (!canonical) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Unknown command text.\" })));\n return;\n }\n const result = orchestrator.applyCommand(canonical);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND) {\n const result = orchestrator.applyCommand(parsed.payload);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n }\n\n if (parsed.type === WS_MESSAGE.INPUT_FRAME) {\n const err = validateInputFrame(parsed.payload, socket);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const frame = parsed.payload.frame;\n const commands = Array.isArray(parsed.payload?.commands) ? parsed.payload.commands : [];\n orchestrator.queueInputFrame(socket.clientId, frame, commands);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, queued: true, frame })));\n }\n\n if (parsed.type === WS_MESSAGE.SIGNAL) {\n const payload = parsed.payload || {};\n const err = validateSignalPayload(payload);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const signalType = payload.signalType;\n const to = payload.to;\n for (const client of wss.clients) {\n if (client.clientId === to && client.readyState === 1) {\n client.send(JSON.stringify(createEnvelope(WS_MESSAGE.SIGNAL, { ...payload, from: socket.clientId })));\n break;\n }\n }\n }\n\n if (parsed.type === WS_MESSAGE.DRONE_TELEMETRY) {\n const payload = parsed.payload || {};\n const json = JSON.stringify(payload);\n if (Buffer.byteLength(json, \"utf8\") > 48 * 1024) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"DRONE_TELEMETRY payload too large.\" })));\n return;\n }\n broadcastExcept(createEnvelope(WS_MESSAGE.DRONE_TELEMETRY, { ...payload, relayFrom: socket.clientId }), socket);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_SOCIAL_PULSE) {\n handleCastleSocialPulse(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_WAL_PEER_FEED) {\n handleCastleWalPeerFeed(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.OPEN_DATA_QUERY) {\n queryOpenData(parsed.payload)\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: true, result })));\n })\n .catch((e) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: false, error: e.message })));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_QUERY) {\n runRhizohBrainV2({\n ...(parsed.payload || {}),\n uid: socket?.auth?.user?.uid || \"unknown\"\n })\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, result)));\n })\n .catch(async () => {\n const fallback = await runRhizohBrain(parsed.payload || {}).catch(() => ({\n ok: false,\n reply: \"Rhizoh temporary disturbance. Habitat remains stable.\"\n }));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, fallback)));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.BROADCAST_REGISTER) {\n const role = String(parsed.payload?.role || \"\");\n if (role === \"GENESIS_BROADCAST_AGENT\") {\n broadcasterClientId = socket.clientId;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, registered: role })));\n broadcastState();\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.STUDIO_CUE) {\n // Only registered broadcaster can push global cues.\n if (!broadcasterClientId || socket.clientId !== broadcasterClientId) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Only broadcaster agent can emit STUDIO_CUE.\" })));\n return;\n }\n const cue = parsed.payload || {};\n if (typeof cue !== \"object\") {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid STUDIO_CUE payload.\" })));\n return;\n }\n broadcast(createEnvelope(WS_MESSAGE.STUDIO_CUE, cue));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_JOIN_ROOM) {\n const requested = String(parsed.payload?.roomId || \"\").trim();\n const requestedName = String(parsed.payload?.roomName || \"\").trim();\n const roomId = ensureRoom(requested || `room-${Math.random().toString(36).slice(2, 7)}`, requestedName || null);\n spiralState.activeByClient.set(socket.clientId, roomId);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, roomId })));\n broadcastSpiralState();\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_CREATE_CHARACTER) {\n const name = String(parsed.payload?.name || \"\").trim().slice(0, 32) || `AI-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;\n const role = String(parsed.payload?.role || \"agent\").trim().slice(0, 24) || \"agent\";\n const roomId = ensureRoom(\n String(parsed.payload?.roomId || spiralState.activeByClient.get(socket.clientId) || spiralState.rooms[0].id),\n null\n );\n\n // Character creation always maps to an authoritative world agent.\n const result = orchestrator.applyCommand({\n command: COMMAND.SPAWN_AGENT,\n params: { targetCastleId: \"castle-01\" }\n });\n if (!result?.ok || !result?.data?.id) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Character creation failed.\" })));\n return;\n }\n const id = result.data.id;\n spiralState.characters.set(id, { id, name, role, roomId, source: \"spawned\" });\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, characterId: id, roomId })));\n broadcastSpiralState();\n return;\n }\n });\n\n socket.on(\"close\", () => {\n if (socket.clientId === broadcasterClientId) broadcasterClientId = null;\n clientStats.delete(socket.clientId);\n spiralState.activeByClient.delete(socket.clientId);\n removeCastleSocialClientFromAllRooms(socket.clientId);\n broadcast(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }));\n broadcastState();\n });\n});\n\nsetInterval(() => {\n void (async () => {\n try {\n const { frame, snapshot, delta, packedDelta } = orchestrator.tick(1 / 20);\n syncSpiralCharactersWithWorld(snapshot);\n broadcast(createEnvelope(WS_MESSAGE.NETWORK_TICK, { frame }));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_TICK, snapshot));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA, delta));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA_PACKED, packedDelta));\n broadcastSpiralState();\n await runAcademyEventTick();\n const executed = await tickAutomations(Date.now());\n for (const a of executed) {\n await addTranscript(a.uid, {\n source: \"planner\",\n eventType: \"automation-executed\",\n text: `Automation executed: ${a.title} (${a.action})`,\n roomId: \"studio-main\",\n meta: { automationId: a.id, action: a.action, deviceId: a.deviceId || \"\" }\n });\n }\n } catch (e) {\n console.error(\"[GATEWAY] world tick / automations loop error:\", String(e?.message || e));\n }\n })();\n}, 50);\n\n(async () => {\n await rhizohRuntime.telemetry.initOpenTelemetry();\n if (String(process.env.CASTLE_GENESIS_DISK_PERSIST ?? \"\").trim() === \"\") {\n process.env.CASTLE_GENESIS_DISK_PERSIST = \"1\";\n }\n const genesisDiskBoot = await hydrateGenesisContinuityPersistenceBootV0();\n if (!genesisDiskBoot.ok) {\n console.warn(\"[GATEWAY] genesis disk hydrate failed; continuing with in-memory genesis continuity\");\n }\n httpServer.listen(PORT, () => {\n startGenesisCanonicalClock();\n installGenesisCheckpointSurfaceGetter(buildGenesisRuntimeSurfacePayloadLive);\n setGenesisContinuityAfterPublishHook((seq) => {\n noteGenesisCheckpointSeqCommitted(seq);\n noteGenesisContinuitySeqAcceptedV0(seq);\n });\n startGenesisContinuityInfraSampler(buildGenesisRuntimeSurfacePayloadLive, 2000);\n logProductionObservatorySurfaceGuardsV0();\n logGatewaySubstrateAuthorityGuardsV0();\n console.log(`[GATEWAY] ws/http://localhost:${PORT}`);\n console.log(\n `[GATEWAY] genesis continuity: GET ${rhizohRuntime.routes.genesisRuntime} | POST ${rhizohRuntime.routes.genesisIngress} | SSE ${rhizohRuntime.routes.genesisStream} | replay ${rhizohRuntime.routes.genesisReplay} | diff ${rhizohRuntime.routes.genesisReplayDiff} | equiv ${rhizohRuntime.routes.genesisReplayEquivalence} | analytics ${rhizohRuntime.routes.genesisReplayAnalytics} | evolution ${rhizohRuntime.routes.genesisReplayEvolution}`\n );\n if (String(process.env.CASTLE_GENESIS_OBSERVATORY_LIVE_V01 ?? \"\").trim() === \"1\") {\n console.log(\"GENESIS OBSERVATORY LIVE v0.1 — READ ONLY PHASE STARTED\");\n }\n });\n})().catch((e) => {\n console.error(\"[GATEWAY] fatal boot (before or during listen):\", e);\n process.exit(1);\n});\n"}, "41ff0616623c5d1c5fbadcf9db0da45cb4ac764d": {"t": 1785754164.0369513, "n": 154368, "h": "e7d17dd82b263dbe", "c": "import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport dotenv from \"dotenv\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconsole.log(\"🔥 [GENESIS_BOOT] entry file:\", __filename);\nconsole.log(\"🔥 [GENESIS_BOOT] node env:\", process.env.NODE_ENV);\nconsole.log(\"🔥 [GENESIS_BOOT] pid:\", process.pid);\nconsole.log(\"🧬 [GENESIS_BOOT] cwd:\", process.cwd());\nconsole.log(\"🧬 [GENESIS_BOOT] import.meta.url:\", import.meta.url);\ndotenv.config({ path: path.join(__dirname, \"..\", \".env\") });\ndotenv.config({ path: path.join(__dirname, \"..\", \".env.local\"), override: true });\n\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport { createServer } from \"node:http\";\nimport { createOrchestrator } from \"@castle/orchestrator\";\nimport { parseTextToCommand } from \"@castle/command-dsl\";\nimport { WS_MESSAGE, COMMAND, createEnvelope, safeJsonParse } from \"@castle/protocol\";\nimport { queryOpenData } from \"./openData.js\";\nimport { verifyClientToken } from \"./auth.js\";\nimport { runRhizohBrain } from \"./rhizohBrain.js\";\nimport { queryRhizohLlm } from \"./rhizohLlmGateway.js\";\nimport { rhizohGatewayTurn } from \"./rhizohGatewayTurn.js\";\nimport {\n RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n runRhizohVoiceTranscribeV3,\n rhizohVoiceTranscribeEnvV3\n} from \"./rhizohVoiceTranscribeV3.js\";\nimport {\n meshAppendDelta,\n meshContinuityAggregate,\n meshJoin,\n meshLeave,\n meshReplayFromNodeId,\n meshReplayFromSeq,\n meshSnapshot,\n meshSubscribeSse\n} from \"./presenceRoomMesh.js\";\nimport { runRhizohBrainV2 } from \"./rhizohBrainV2.js\";\nimport {\n listConnections,\n createConnection,\n updateConnection,\n deleteConnection,\n setDefaultConnection,\n resolveConnection\n} from \"./llmConnectionsStore.js\";\nimport { checkHttpRateLimit, getHttpClientIp } from \"./castleHttpRateLimit.js\";\nimport {\n submitAbuseReportV0,\n listModerationQueueV0,\n softBlockUserV0,\n removeSoftBlockV0,\n listSoftBlockedUsersV0\n} from \"./ops/moderationMvpV0.js\";\nimport { listRecentAgentSnapshotsV0 } from \"./ops/agentObservabilityV0.js\";\nimport { readAgentContainmentConfigV0 } from \"./ops/agentContainmentV0.js\";\nimport { readCostContainmentConfigV0, getCostContainmentStatsV0 } from \"./ops/costContainmentV0.js\";\nimport { getGlobalCostLedgerSnapshotV0, getGclBackendStatusV0 } from \"./ops/globalCostLedgerV0.js\";\nimport { evaluateEconomicStrategyL3V0 } from \"./ops/economicStrategyEngineL3V0.js\";\nimport { getPhasedRolloutStatsV0, readPhasedRolloutTierV0 } from \"./ops/phasedRolloutV0.js\";\nimport { buildUnifiedStateNarrativeV0 } from \"./ops/unifiedStateNarrativeV0.js\";\nimport { resolveHardeningTenantIdV0 } from \"./ops/narrativeTenantIsolationV0.js\";\nimport {\n classifyStressResponseV0,\n verifyStressTaxonomyCoverageV0,\n verifyStressConflictResolutionV0\n} from \"./ops/stressResponseTaxonomyV0.js\";\nimport { verifyResolutionStabilityEnvelopeV0 } from \"./ops/resolutionStabilityEnvelopeV0.js\";\nimport { logLlmAccess } from \"./castleLlmAudit.js\";\nimport { appendMemory, listMemories, getMemoryContext, getPersonaGoalMemory, setPersonaGoalMemory, autoCompactMemories } from \"./memoryStore.js\";\nimport { getFirebasePersistence } from \"./firebasePersistence.js\";\nimport { registerAgentIdentity, listAgentIdentities, getAgentIdentity, updateAgentIdentity } from \"./agentIdentityStore.js\";\nimport { queueAcademyEvent, listAcademyEvents, resolveAcademyEvent, runAcademyEventTick } from \"./academyEventEngine.js\";\nimport { enqueuePdfJob, getPdfJob } from \"./pdfPipeline.js\";\nimport {\n createPublishSession,\n listPublishSessions,\n updatePublishSession,\n addTranscript,\n listTranscripts,\n searchMetadata,\n registerDevice,\n listDevices,\n updateDevice,\n appendTelemetry,\n listTelemetry,\n createAutomation,\n listAutomations,\n tickAutomations,\n enqueueCommand,\n listCommands,\n updateCommandStatus,\n registerSocialChannel,\n listSocialChannels\n} from \"./studioOpsStore.js\";\nimport { metrics as infraMetrics } from \"./infra/metrics.js\";\nimport { renderRhizohPrometheusMetrics, rhizohEnterpriseMetrics } from \"./infra/rhizohEnterpriseMetrics.js\";\nimport { scoreHealth } from \"./infra/healthScore.js\";\nimport { getTraceById, listTracesBySession } from \"./infra/traceRegistry.js\";\nimport { canonicalEpistemicSealString, hashAndSignEpistemicSeal, EPISTEMIC_SEAL_SCHEMA } from \"./epistemicSeal.js\";\nimport { persistEpistemicLedgerBatch } from \"./epistemicLedgerStore.js\";\nimport { persistEpistemicForecastBatch } from \"./epistemicForecastStore.js\";\nimport { buildRhizohExternalGroundTruthPayload } from \"./rhizohExternalGroundTruthGateway.js\";\nimport { ingestRhizohExternalLossBatchHttp } from \"./rhizohExternalLossIngestGateway.js\";\nimport {\n ingestRhizohProductOutcomeHttp,\n listRhizohProductOutcomeAggregates,\n verifyRhizohOutcomeHmac,\n verifyRhizohOutcomeSourceToken\n} from \"./rhizohProductOutcomeIngestGateway.js\";\nimport { initRhizoh } from \"./rhizohProductionBootstrap.js\";\nimport { handleAcademicObservatoryExportGetV0 } from \"./rhizoh/academicObservatoryHttpV0.js\";\nimport {\n buildGenesisRuntimeSurfacePayload,\n recordGenesisEpistemicLedgerPersisted,\n recordGenesisEpistemicSealIssued,\n startGenesisCanonicalClock\n} from \"./genesisRuntimeSurfaceV0.js\";\nimport { registerGenesisSseClient, getGenesisContinuitySeq, setGenesisContinuityAfterPublishHook } from \"./genesisContinuityStreamHubV0.js\";\nimport { computeGenesisReplayFingerprintV0 } from \"./genesisReplayFingerprintV0.js\";\nimport { buildGenesisGatewayCapabilitiesV0 } from \"./genesisGatewayCapabilitiesV0.js\";\nimport {\n installGenesisCheckpointSurfaceGetter,\n noteGenesisCheckpointSeqCommitted,\n getLatestGenesisSignedCheckpoint\n} from \"./genesisContinuityCheckpointV0.js\";\nimport { hydrateGenesisContinuityPersistenceBootV0 } from \"./genesisContinuityHydrateBootV0.js\";\nimport { startGenesisContinuityInfraSampler } from \"./genesisContinuityInfraSamplerV0.js\";\nimport {\n genesisCheckpointQueryBySeqV0,\n genesisCheckpointQueryRangeV0,\n genesisCheckpointQueryLineageV0\n} from \"./genesisCheckpointQueryV0.js\";\nimport {\n genesisContinuityEventArchiveEnabled,\n queryGenesisContinuityEventArchiveV0,\n GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION\n} from \"./genesisContinuityEventArchiveV0.js\";\nimport { resolveGenesisReplayRouterV1, GENESIS_REPLAY_ROUTER_SCHEMA } from \"./genesisReplayRouterV1.js\";\nimport {\n computeGenesisReplayTemporalDiffV1,\n GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA\n} from \"./genesisReplayTemporalDiffV1.js\";\nimport { compareGenesisReplayEquivalenceV1 } from \"./genesisReplayEquivalenceV1.js\";\nimport { computeGenesisReplayAnalyticsV1 } from \"./genesisReplayAnalyticsV1.js\";\nimport { computeGenesisReplayEvolutionV1 } from \"./genesisReplayEvolutionV1.js\";\nimport { handleStripeCheckoutCreate, handleStripeMembershipWebhook } from \"./stripeWebhookMembership.js\";\nimport { buildRealityHealthPayload } from \"./edgeRealityConsistencyV1.js\";\nimport {\n assertWalPeerSocketAuthorityV0,\n buildGatewaySubstrateAuthoritySnapshotV0,\n logGatewaySubstrateAuthorityGuardsV0,\n validateWalPeerFeedSignatureV0\n} from \"./gatewaySubstrateAuthorityV0.js\";\nimport {\n buildSubstrateOperationalSnapshotV0,\n recordWalPeerRejectV0,\n renderSubstratePrometheusMetricsV0\n} from \"./infra/substrateOperationalMetrics.js\";\nimport { ingestClientSubstrateHealthV0 } from \"./rhizohSubstrateHealthIngestV0.js\";\nimport { recordSubstrateOtelEventV0 } from \"./infra/opentelemetrySubstrateV0.js\";\nimport {\n GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n getGenesisWorldObservationIngressStatusV0,\n ingestGenesisWorldObservationV0\n} from \"./genesisContinuityClientIngressV0.js\";\nimport {\n getGenesisContinuitySeqAuditV0,\n noteGenesisContinuitySeqAcceptedV0\n} from \"./genesisContinuitySeqGapV0.js\";\n\n/** Render / Fly / Railway: `PORT` — yoksa `CASTLE_GATEWAY_PORT` — yoksa 8090. */\nconst PORT =\n Number(process.env.PORT) ||\n Number(process.env.CASTLE_GATEWAY_PORT) ||\n 8090;\n/** Spiral / world WebSocket server — set after `wss` init; used by genesis runtime surface. */\nlet spiralWssForGenesis = null;\nconst MAX_MESSAGE_BYTES = Number(process.env.CASTLE_MAX_MESSAGE_BYTES || 32 * 1024);\nconst REQUIRED_GATEWAY_TOKEN = process.env.CASTLE_GATEWAY_TOKEN || \"\";\nconst ALLOWED_ORIGINS = (process.env.CASTLE_ALLOWED_ORIGINS || \"\")\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n\nfunction normalizeHttpOrigin(origin) {\n return String(origin || \"\")\n .trim()\n .replace(/\\/+$/, \"\");\n}\n\n/** HTTP CORS: `CASTLE_ALLOWED_ORIGINS` + `CASTLE_HTTP_CORS_ORIGIN` (tek origin); `CASTLE_HTTP_CORS_ORIGIN=*` → wildcard. */\nfunction buildHttpCorsOriginAllowSet() {\n const set = new Set();\n for (const o of ALLOWED_ORIGINS) {\n const n = normalizeHttpOrigin(o);\n if (n) set.add(n);\n }\n const primary = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n if (primary && primary !== \"*\") set.add(primary);\n return set;\n}\n\nconst HTTP_CORS_ORIGIN_ALLOW_SET = buildHttpCorsOriginAllowSet();\n\n/**\n * Tarayıcı `Origin` ile tam eşleşme ister. Tek sabit `CASTLE_HTTP_CORS_ORIGIN` yeterli değil:\n * örn. `web.app` vs `firebaseapp.com` veya Render’da yanlış/eksik env.\n * @returns {string|null} `Access-Control-Allow-Origin` değeri; null = başlık yok (istemci CORS fail).\n */\nfunction accessControlAllowOriginValue(req) {\n const primaryEnv = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n if (primaryEnv === \"*\") return \"*\";\n const reqOrigin = normalizeHttpOrigin(req.headers?.origin);\n if (HTTP_CORS_ORIGIN_ALLOW_SET.size === 0) return \"*\";\n if (!reqOrigin) return \"*\";\n if (HTTP_CORS_ORIGIN_ALLOW_SET.has(reqOrigin)) return reqOrigin;\n return null;\n}\n\nfunction applyHttpCorsHeaders(req, res) {\n const allow = accessControlAllowOriginValue(req);\n if (allow) {\n res.setHeader(\"Access-Control-Allow-Origin\", allow);\n if (allow !== \"*\") res.setHeader(\"Vary\", \"Origin\");\n }\n res.setHeader(\"Access-Control-Allow-Methods\", \"GET,POST,OPTIONS\");\n res.setHeader(\n \"Access-Control-Allow-Headers\",\n \"Content-Type, Authorization, X-Castle-Dev-Uid, X-Castle-Guest-Id, X-Castle-Gateway-Token, X-Castle-Ingress-Contract, X-Rhizoh-Outcome-Signature, X-Rhizoh-Outcome-Source-Token, X-Castle-Academic-Observatory-Key, X-Castle-Moderation-Key\"\n );\n}\nconst REQUIRE_AUTH = process.env.CASTLE_REQUIRE_AUTH === \"true\";\nconst ALLOW_DEV_ANON = process.env.CASTLE_ALLOW_DEV_ANON !== \"false\";\nconst ALLOW_DEV_HTTP_UID = process.env.CASTLE_ALLOW_DEV_HTTP_UID !== \"false\";\n/** Presence mesh: allow `X-Castle-Guest-Id` when Firebase auth absent (default on). Set `false` to require auth only. */\nconst ALLOW_MESH_GUEST = process.env.CASTLE_ALLOW_MESH_GUEST !== \"false\";\nconst SOCIAL_RETRY_MAX = Math.max(0, Math.min(5, Number(process.env.CASTLE_SOCIAL_RETRY_MAX || 2)));\nconst SOCIAL_RETRY_BASE_MS = Math.max(100, Number(process.env.CASTLE_SOCIAL_RETRY_BASE_MS || 700));\nconst TELEGRAM_BOT_TOKEN = process.env.CASTLE_TELEGRAM_BOT_TOKEN || \"\";\nconst WHATSAPP_TOKEN = process.env.CASTLE_WHATSAPP_TOKEN || \"\";\nconst WHATSAPP_PHONE_NUMBER_ID = process.env.CASTLE_WHATSAPP_PHONE_NUMBER_ID || \"\";\nconst RL_RHIZOH_LLM_PER_MIN = Math.max(5, Number(process.env.CASTLE_RL_RHIZOH_LLM_PER_MIN || 40));\nconst RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN || 30)\n);\nconst RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN || 120));\nconst RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN = Math.max(8, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN || 48));\nconst RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN = Math.max(4, Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN || 24));\nconst RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN || 18)\n);\nconst RL_EPISTEMIC_SEAL_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_EPISTEMIC_SEAL_PER_MIN || 120));\nconst RL_EPISTEMIC_LOG_PER_MIN = Math.max(20, Number(process.env.CASTLE_RL_EPISTEMIC_LOG_PER_MIN || 240));\nconst EPISTEMIC_SEAL_SECRET = String(\n process.env.CASTLE_EPISTEMIC_SEAL_SECRET || process.env.CASTLE_GATEWAY_TOKEN || \"\"\n).trim();\nconst RL_LLM_CONN_TEST_PER_MIN = Math.max(2, Number(process.env.CASTLE_RL_LLM_CONNECTION_TEST_PER_MIN || 8));\n\n/** One-shot production warnings: CORS surface, auth toggle, dev bypass flags (observatory deploy hygiene). */\nfunction logProductionObservatorySurfaceGuardsV0() {\n if (String(process.env.NODE_ENV || \"\").toLowerCase() !== \"production\") return;\n const tag = \"[GATEWAY_PROD_GUARD]\";\n const primaryCors = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n const corsListEmpty = ALLOWED_ORIGINS.length === 0;\n if (corsListEmpty && !primaryCors) {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty and CASTLE_HTTP_CORS_ORIGIN empty — browser SSE/replay may fail; set explicit Firebase Hosting origins`\n );\n } else if (corsListEmpty && primaryCors && primaryCors !== \"*\") {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty — only CASTLE_HTTP_CORS_ORIGIN set; add web.app + firebaseapp.com (and previews) to CASTLE_ALLOWED_ORIGINS if clients use alternate hostnames`\n );\n }\n if (primaryCors && /localhost|127\\.0\\.0\\.1/i.test(primaryCors)) {\n console.warn(`${tag} CORS: CASTLE_HTTP_CORS_ORIGIN is localhost while NODE_ENV=production`);\n }\n if (REQUIRE_AUTH) {\n console.warn(\n `${tag} AUTH: CASTLE_REQUIRE_AUTH=true — clients must send a valid gateway credential (otherwise silent 401 on replay/SSE); EventSource cannot set custom headers unless proxied`\n );\n if (REQUIRED_GATEWAY_TOKEN.length < 16) {\n console.warn(`${tag} AUTH: CASTLE_GATEWAY_TOKEN missing or short while require-auth enabled`);\n }\n }\n if (ALLOW_DEV_ANON) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_ANON is not \"false\" — dev anonymous path may be live in production (set CASTLE_ALLOW_DEV_ANON=false)`\n );\n }\n if (ALLOW_DEV_HTTP_UID) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_HTTP_UID is not \"false\" — X-Castle-Dev-Uid bypass risk (set CASTLE_ALLOW_DEV_HTTP_UID=false)`\n );\n }\n}\n\nconst rhizohRuntime = initRhizoh();\n\n/** İstemci: llmKeySource veya keySource — env | user_connection | auto */\nfunction normalizeClientLlmKeySource(raw) {\n const s = String(raw ?? \"\").trim().toLowerCase();\n if (s === \"env\" || s === \"server\") return \"env\";\n if (s === \"user_connection\" || s === \"connection\" || s === \"user\") return \"user_connection\";\n if (s === \"auto\" || s === \"\") return \"auto\";\n return null;\n}\n\nfunction sendJson(res, statusCode, payload) {\n res.writeHead(statusCode, { \"Content-Type\": \"application/json; charset=utf-8\" });\n res.end(JSON.stringify(payload));\n}\n\nfunction logRhizohHealth(stage, detail = {}) {\n try {\n console.info(`[CASTLE_${String(stage || \"unknown\")}]`, detail && typeof detail === \"object\" ? detail : {});\n } catch {\n /* noop */\n }\n}\n\nfunction renderInfraMetricsProm() {\n const lines = [\n `castle_gateway_events_processed_total ${infraMetrics.eventsProcessed}`,\n `castle_gateway_duplicate_reject_total ${infraMetrics.duplicateRejects}`,\n `castle_gateway_enqueue_latency_ms ${infraMetrics.enqueueLatencyMs}`,\n `castle_gateway_queue_depth ${infraMetrics.queueDepth}`,\n `castle_gateway_queue_lag_seconds ${(Number(infraMetrics.queueLag || 0) / 1000).toFixed(3)}`,\n `castle_gateway_errors_total ${infraMetrics.errors}`\n ];\n const buckets = infraMetrics.enqueueLatencyBuckets || {};\n for (const k of Object.keys(buckets)) {\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"${k}\"} ${buckets[k]}`);\n }\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"+Inf\"} ${infraMetrics.eventsProcessed}`);\n if (process.env.CASTLE_SUBSTRATE_METRICS !== \"0\") {\n lines.push(renderSubstratePrometheusMetricsV0().trimEnd());\n }\n return lines.join(\"\\n\");\n}\n\nasync function fetchJsonWithTimeout(url, timeoutMs = 1200) {\n const ctl = new AbortController();\n const tid = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const res = await fetch(url, { signal: ctl.signal });\n if (!res.ok) return null;\n return await res.json();\n } catch {\n return null;\n } finally {\n clearTimeout(tid);\n }\n}\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction getHttpPathname(req) {\n let p = String(req.url || \"\").split(\"?\")[0];\n /* Reverse proxies / clients sometimes append `/` — avoid silent 404 on e.g. `/rhizoh/genesis/stream/`. */\n while (p.length > 1 && p.endsWith(\"/\")) {\n p = p.slice(0, -1);\n }\n return p;\n}\n\nfunction rhizohLlmEnvConfigured() {\n const p = String(process.env.CASTLE_LLM_PROVIDER || \"openai\").toLowerCase();\n const key =\n p === \"anthropic\"\n ? process.env.ANTHROPIC_API_KEY\n : p === \"gemini\"\n ? process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY\n : p === \"xai\"\n ? process.env.XAI_API_KEY\n : p === \"deepseek\"\n ? process.env.DEEPSEEK_API_KEY\n : p === \"mistral\"\n ? process.env.MISTRAL_API_KEY\n : p === \"openrouter\"\n ? process.env.OPENROUTER_API_KEY\n : process.env.OPENAI_API_KEY;\n return !!String(key || \"\").trim();\n}\n\n/**\n * @param {import(\"firebase-admin/firestore\").Firestore | null} db\n * @param {number} timeoutMs\n */\nasync function probeFirestoreReachable(db, timeoutMs = 2000) {\n if (!db) return { ok: false, reason: \"no_db\" };\n const timeout = sleep(timeoutMs).then(() => ({ ok: false, reason: \"timeout\" }));\n const work = db\n .collection(\"_castle_gateway_health\")\n .doc(\"ping\")\n .get()\n .then(() => ({ ok: true, reason: \"reachable\" }))\n .catch((e) => ({ ok: false, reason: String(e?.message || e || \"firestore_error\") }));\n return Promise.race([work, timeout]);\n}\n\nfunction stripHtml(text = \"\") {\n return String(text)\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \" \")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \" \")\n .replace(/<[^>]+>/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction haversineMeters(aLat, aLon, bLat, bLon) {\n const R = 6371000;\n const dLat = ((bLat - aLat) * Math.PI) / 180;\n const dLon = ((bLon - aLon) * Math.PI) / 180;\n const sa = Math.sin(dLat / 2);\n const sb = Math.sin(dLon / 2);\n const x = sa * sa + Math.cos((aLat * Math.PI) / 180) * Math.cos((bLat * Math.PI) / 180) * sb * sb;\n return 2 * R * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));\n}\n\nasync function checkPublishTargetHealth(protocol, target) {\n const p = String(protocol || \"\").toUpperCase();\n const t = String(target || \"\").trim();\n if (!t) return { ok: false, reason: \"target_missing\", shouldReconnect: false };\n if (p === \"RTMP\") {\n const ok = t.startsWith(\"rtmp://\") || t.startsWith(\"rtmps://\");\n return { ok, reason: ok ? \"rtmp_target_format_ok\" : \"invalid_rtmp_target\", shouldReconnect: !ok, reconnectInSec: 15 };\n }\n if (p === \"WHIP\" || p === \"SFU\") {\n try {\n const res = await fetch(t, { method: \"OPTIONS\" });\n const ok = res.status < 500;\n return { ok, reason: `http_${res.status}`, shouldReconnect: !ok, reconnectInSec: 8 };\n } catch {\n return { ok: false, reason: \"network_unreachable\", shouldReconnect: true, reconnectInSec: 8 };\n }\n }\n return { ok: true, reason: \"unchecked_protocol\", shouldReconnect: false };\n}\n\nasync function fetchTextFromUrl(url) {\n const res = await fetch(url, { method: \"GET\" });\n if (!res.ok) throw new Error(`fetch_${res.status}`);\n const ct = String(res.headers.get(\"content-type\") || \"\");\n if (ct.includes(\"application/pdf\")) throw new Error(\"pdf_binary_not_supported_directly\");\n const body = await res.text();\n return stripHtml(body).slice(0, 14000);\n}\n\nfunction buildTaskActionGraph(task = \"\", context = {}) {\n const t = String(task || \"\").toLowerCase();\n if (t.includes(\"dinner\") || t.includes(\"akşam yemeği\") || t.includes(\"aksam yemegi\")) {\n return {\n taskTitle: \"Dinner Prep Planner\",\n nodes: [\n { id: \"n1\", title: \"Kitchen preheat\", action: \"smart-oven.preheat\", params: { temperatureC: 190, mode: \"fan\" } },\n { id: \"n2\", title: \"Lighting comfort\", action: \"smart-light.set_scene\", params: { scene: \"warm_dinner\" } },\n { id: \"n3\", title: \"Fridge inventory check\", action: \"smart-fridge.inventory_scan\", params: {} },\n { id: \"n4\", title: \"Reminder notification\", action: \"smart-watch.notify\", params: { text: \"Dinner prep started.\" } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" },\n { from: \"n3\", to: \"n4\" }\n ]\n };\n }\n return {\n taskTitle: task || \"General Planner\",\n nodes: [\n { id: \"n1\", title: \"Safety check\", action: \"device.safety_check\", params: { context } },\n { id: \"n2\", title: \"Primary action\", action: \"device.execute_task\", params: { task } },\n { id: \"n3\", title: \"Report\", action: \"studio.timeline_note\", params: { task } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" }\n ]\n };\n}\n\nfunction buildEthicsPrograms() {\n return {\n policyVersion: \"v1\",\n tracks: [\n {\n id: \"elderly-care-assist\",\n audience: \"elderly\",\n title: \"Elderly Assistance Track\",\n principles: [\"human-in-the-loop\", \"consent-first\", \"low-friction alerts\"],\n modules: [\"medication-reminder\", \"fall-risk-check\", \"family-notify\", \"daily-companion-brief\"]\n },\n {\n id: \"kids-safe-learning\",\n audience: \"children\",\n title: \"Kids Safe Learning Track\",\n principles: [\"guardian-consent\", \"age-appropriate-language\", \"content-filtering\"],\n modules: [\"study-coach\", \"screen-time-balance\", \"safe-social-share\", \"emergency-contact\"]\n }\n ]\n };\n}\n\nasync function deliverViaTelegram(channel, text) {\n const chatId = String(channel.endpoint || \"\").trim();\n if (!TELEGRAM_BOT_TOKEN) throw new Error(\"telegram_token_missing\");\n if (!chatId) throw new Error(\"telegram_chat_id_missing\");\n const url = `https://api.telegram.org/bot${encodeURIComponent(TELEGRAM_BOT_TOKEN)}/sendMessage`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ chat_id: chatId, text })\n });\n if (!res.ok) throw new Error(`telegram_http_${res.status}`);\n return { provider: \"telegram\", status: res.status };\n}\n\nasync function deliverViaWhatsapp(channel, text) {\n const to = String(channel.endpoint || \"\").trim();\n if (!WHATSAPP_TOKEN) throw new Error(\"whatsapp_token_missing\");\n if (!WHATSAPP_PHONE_NUMBER_ID) throw new Error(\"whatsapp_phone_number_id_missing\");\n if (!to) throw new Error(\"whatsapp_to_missing\");\n const url = `https://graph.facebook.com/v22.0/${encodeURIComponent(WHATSAPP_PHONE_NUMBER_ID)}/messages`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${WHATSAPP_TOKEN}` },\n body: JSON.stringify({ messaging_product: \"whatsapp\", to, type: \"text\", text: { body: text } })\n });\n if (!res.ok) throw new Error(`whatsapp_http_${res.status}`);\n return { provider: \"whatsapp\", status: res.status };\n}\n\nasync function deliverViaWebhook(channel, text) {\n const endpoint = String(channel.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"webhook_endpoint_missing\");\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text, platform: channel.platform || \"webhook\", ts: Date.now() })\n });\n if (!res.ok) throw new Error(`webhook_http_${res.status}`);\n return { provider: \"webhook\", status: res.status };\n}\n\nasync function deliverSocialMessage(channel, text) {\n const platform = String(channel.platform || \"webhook\").toLowerCase();\n if (platform === \"telegram\") return deliverViaTelegram(channel, text);\n if (platform === \"whatsapp\") return deliverViaWhatsapp(channel, text);\n return deliverViaWebhook(channel, text);\n}\n\nasync function deliverSocialWithRetry(channel, text) {\n let attempt = 0;\n let lastError = null;\n while (attempt <= SOCIAL_RETRY_MAX) {\n try {\n const result = await deliverSocialMessage(channel, text);\n return { ok: true, attempt: attempt + 1, ...result };\n } catch (error) {\n lastError = error;\n if (attempt >= SOCIAL_RETRY_MAX) break;\n const jitter = Math.floor(Math.random() * 90);\n await sleep(SOCIAL_RETRY_BASE_MS * Math.pow(2, attempt) + jitter);\n }\n attempt += 1;\n }\n return { ok: false, attempt: attempt + 1, error: String(lastError?.message || \"delivery_failed\") };\n}\n\nasync function dispatchCommandToAdapter(device, command) {\n const adapter = String(command?.adapter || device?.adapter || \"websocket\").toLowerCase();\n const endpoint = String(device?.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"device_endpoint_missing\");\n if (adapter === \"mqtt\") {\n // MQTT delivery is proxied via a bridge endpoint (HTTP webhook style).\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n type: \"mqtt-command\",\n topic: `castle/devices/${device.id}/commands`,\n action: command.action,\n params: command.params || {}\n })\n });\n if (!res.ok) throw new Error(`mqtt_bridge_http_${res.status}`);\n return { ok: true, adapter, via: \"http-bridge\", status: res.status };\n }\n if (!endpoint.startsWith(\"ws://\") && !endpoint.startsWith(\"wss://\")) throw new Error(\"invalid_websocket_endpoint\");\n const sendResult = await new Promise((resolve, reject) => {\n const ws = new WebSocket(endpoint);\n const timer = setTimeout(() => {\n try {\n ws.close();\n } catch {\n /* noop */\n }\n reject(new Error(\"websocket_timeout\"));\n }, 3000);\n ws.on(\"open\", () => {\n ws.send(\n JSON.stringify({\n type: \"castle-command\",\n commandId: command.id,\n deviceId: command.deviceId,\n action: command.action,\n params: command.params || {}\n }),\n (err) => {\n clearTimeout(timer);\n ws.close();\n if (err) reject(new Error(\"websocket_send_failed\"));\n else resolve({ ok: true, adapter: \"websocket\", via: \"ws\" });\n }\n );\n });\n ws.on(\"error\", () => {\n clearTimeout(timer);\n reject(new Error(\"websocket_connect_failed\"));\n });\n });\n return sendResult;\n}\n\nfunction readHttpJson(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => {\n if (!buf) return resolve({});\n const payload = safeJsonParse(buf);\n if (!payload || typeof payload !== \"object\") return reject(new Error(\"invalid_json_payload\"));\n resolve(payload);\n });\n req.on(\"error\", reject);\n });\n}\n\n/** Ham gövde (HMAC doğrulaması için). */\nfunction readHttpBodyText(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => resolve(buf));\n req.on(\"error\", reject);\n });\n}\n\nfunction maskOpaqueId(value) {\n const t = String(value || \"\");\n if (!t) return \"(empty)\";\n if (t.length <= 8) return \"****\";\n return `${t.slice(0, 4)}…${t.slice(-2)}`;\n}\n\nfunction readGatewayHttpToken(req) {\n const x = String(req.headers?.[\"x-castle-gateway-token\"] || \"\").trim();\n if (x) return x;\n const auth = String(req.headers?.authorization || \"\");\n const m = /^Bearer\\s+(.+)$/i.exec(auth);\n return m ? String(m[1] || \"\").trim() : \"\";\n}\n\nasync function resolveHttpUser(req) {\n const authResult = await verifyClientToken(req);\n if (authResult?.ok && authResult.user?.uid) {\n return { ok: true, uid: String(authResult.user.uid), mode: authResult.kind || \"auth\" };\n }\n if (ALLOW_DEV_ANON && ALLOW_DEV_HTTP_UID) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) return { ok: true, uid: `dev-${devUid.slice(0, 64)}`, mode: \"dev-header\" };\n }\n return { ok: false, reason: authResult?.reason || \"auth_required\" };\n}\n\n/**\n * Epistemic ledger ingest — Firebase uid OR valid transport token + X-Castle-Dev-Uid.\n * Prod disables CASTLE_ALLOW_DEV_HTTP_UID; LLM may still work while telemetry 401'd without this.\n */\nasync function resolveEpistemicIngestActor(req) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok && tok === REQUIRED_GATEWAY_TOKEN) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) {\n return { ok: true, uid: `gw-telemetry-${devUid.slice(0, 64)}`, mode: \"gateway-token-dev-uid\" };\n }\n return { ok: false, reason: \"dev_uid_required\" };\n }\n if (tok) {\n return { ok: false, reason: \"gateway_token_mismatch\" };\n }\n }\n const auth = await resolveHttpUser(req);\n if (auth.ok) return auth;\n return { ok: false, reason: auth.reason || \"auth_required\" };\n}\n\n/** Mesh actor: Firebase uid, dev header, or `guest:<uuid>` via header / `guestId` query (SSE cannot set headers). */\nasync function resolveMeshActor(req) {\n const auth = await resolveHttpUser(req);\n if (auth.ok) return { ok: true, clientUid: auth.uid, mode: auth.mode };\n if (ALLOW_MESH_GUEST) {\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const gid = String(req.headers?.[\"x-castle-guest-id\"] || q.searchParams.get(\"guestId\") || \"\").trim();\n if (gid) {\n const normalized = gid.startsWith(\"guest:\") ? gid.slice(0, 180) : `guest:${gid.slice(0, 160)}`;\n return { ok: true, clientUid: normalized, mode: \"guest-header\" };\n }\n }\n return { ok: false, reason: \"mesh_auth_required\" };\n}\n\n/** Live genesis runtime JSON (GET /rhizoh/genesis/runtime + SSE infra sampler). */\nasync function buildGenesisRuntimeSurfacePayloadLive() {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n let spiralWsActive = 0;\n if (spiralWssForGenesis) {\n for (const c of spiralWssForGenesis.clients) {\n if (c.readyState === 1) spiralWsActive += 1;\n }\n }\n const base = buildGenesisRuntimeSurfacePayload({\n infraMetrics,\n scoredHealth: scored,\n rhizohEnterpriseMetrics,\n mesh: meshContinuityAggregate(),\n workerHealth: worker,\n port: PORT,\n spiralWebSocketClientsActive: spiralWsActive\n });\n return {\n ...base,\n replayFingerprint: computeGenesisReplayFingerprintV0(base),\n gatewayCapabilities: buildGenesisGatewayCapabilitiesV0(),\n genesisStream: {\n schema: \"castle.genesis.stream_cursor.v0\",\n lastAcceptedSeq: getGenesisContinuitySeq(),\n clientIngress: {\n schema: GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n contract: \"castle.world_observation.ingress_envelope.v1\",\n postPath: rhizohRuntime.routes.genesisIngress,\n closure: getGenesisWorldObservationIngressStatusV0()\n },\n seqContinuity: getGenesisContinuitySeqAuditV0(),\n eventArchive: genesisContinuityEventArchiveEnabled()\n ? {\n enabled: true,\n projection: GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION,\n queryPath: rhizohRuntime.routes.genesisContinuityEvents\n }\n : { enabled: false },\n replayRouter: {\n schema: GENESIS_REPLAY_ROUTER_SCHEMA,\n queryPath: rhizohRuntime.routes.genesisReplay,\n queryHint: \"from=&to= or range=from-to; optional type=&checkpoints=0\",\n temporalDiffPath: rhizohRuntime.routes.genesisReplayDiff,\n temporalDiffSchema: GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA,\n equivalencePath: rhizohRuntime.routes.genesisReplayEquivalence,\n analyticsPath: rhizohRuntime.routes.genesisReplayAnalytics,\n evolutionPath: rhizohRuntime.routes.genesisReplayEvolution\n }\n }\n };\n}\n\nconst httpServer = createServer(async (req, res) => {\n applyHttpCorsHeaders(req, res);\n if (req.method === \"OPTIONS\") {\n res.writeHead(204);\n res.end();\n return;\n }\n\n const pathname = getHttpPathname(req);\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/genesis/__ping\") {\n sendJson(res, 200, {\n ok: true,\n pid: process.pid,\n entry: __filename,\n time: Date.now(),\n genesis: true\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/epistemic/__probe\") {\n sendJson(res, 200, {\n ok: true,\n epistemicIngestActorV2: true,\n routes: {\n seal: rhizohRuntime.routes.epistemicSeal,\n logsBatch: rhizohRuntime.routes.epistemicLogsBatch\n },\n hasGatewayToken: Boolean(REQUIRED_GATEWAY_TOKEN),\n gatewayTokenLen: REQUIRED_GATEWAY_TOKEN ? REQUIRED_GATEWAY_TOKEN.length : 0\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/webhooks/stripe\") {\n await handleStripeMembershipWebhook(req, res);\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/billing/stripe/checkout\") {\n await handleStripeCheckoutCreate(req, res, { readHttpJson, resolveHttpUser, sendJson });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/live\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, live: false, dns: true, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n sendJson(res, 200, { ok: true, live: true, dns: true, service: \"castle-gateway\", ts: Date.now() });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/ready\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, ready: false, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n try {\n const persistence = getFirebasePersistence();\n const llmProbe = rhizohLlmEnvConfigured();\n sendJson(res, 200, {\n ok: true,\n ready: true,\n dns: true,\n persistence: persistence.mode,\n service: \"castle-gateway\",\n gateway: \"ok\",\n llm: llmProbe ? \"ok\" : \"degraded\",\n spine: llmProbe ? \"ok\" : \"partial\"\n });\n } catch (e) {\n sendJson(res, 503, { ok: false, ready: false, reason: String(e?.message || e), service: \"castle-gateway\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/reality\") {\n const payload = buildRealityHealthPayload();\n sendJson(res, payload.ok ? 200 : 503, payload);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/substrate\") {\n const authority = buildGatewaySubstrateAuthoritySnapshotV0();\n const operational = buildSubstrateOperationalSnapshotV0();\n const drift = buildRealityHealthPayload();\n const ok = Boolean(authority.productionAuthoritySatisfied) && drift.ok;\n sendJson(res, ok ? 200 : 503, {\n ok,\n service: \"castle-gateway\",\n ts: Date.now(),\n authority,\n operational,\n drift: { ok: drift.ok, errors: drift.drift?.errors, warnings: drift.drift?.warnings }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/substrate/health\") {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const body = await readHttpJson(req);\n const uid = (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestClientSubstrateHealthV0(body, { uid, clientId: uid });\n if (!result.ok) {\n sendJson(res, 400, result);\n return;\n }\n recordSubstrateOtelEventV0(\"client_health_ingest\", { uid: String(uid).slice(0, 32) });\n sendJson(res, 200, result);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/deps\") {\n const t0 = Date.now();\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, {\n ok: false,\n dns: true,\n llm: false,\n firestore: false,\n phase: \"maintenance\",\n latencyMs: Date.now() - t0,\n service: \"castle-gateway\"\n });\n return;\n }\n const persistence = getFirebasePersistence();\n let firestoreOk = persistence.mode !== \"firebase\";\n let firestoreDetail = persistence.mode === \"file\" ? \"file_mode\" : \"not_applicable\";\n if (persistence.mode === \"firebase\" && persistence.db) {\n const pr = await probeFirestoreReachable(persistence.db, 2000);\n firestoreOk = pr.ok;\n firestoreDetail = pr.reason || (pr.ok ? \"reachable\" : \"fail\");\n } else if (persistence.mode === \"firebase\" && !persistence.db) {\n firestoreOk = false;\n firestoreDetail = \"firebase_not_initialized\";\n }\n const llmOk = rhizohLlmEnvConfigured();\n const overallOk = llmOk && firestoreOk;\n const latencyMs = Date.now() - t0;\n sendJson(res, 200, {\n ok: overallOk,\n dns: true,\n llm: llmOk,\n firestore: persistence.mode === \"firebase\" ? firestoreOk : false,\n persistence: persistence.mode,\n firestoreDetail,\n latencyMs,\n service: \"castle-gateway\",\n wsPort: PORT\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health\") {\n const persistence = getFirebasePersistence();\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n wsPort: PORT,\n persistence: persistence.mode,\n presenceMesh: \"sse\"\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/health\") {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n sendJson(res, 200, {\n ok: true,\n role: \"gateway\",\n queueLagMs: infraMetrics.queueLag,\n queueDepth: infraMetrics.queueDepth,\n errors: infraMetrics.errors,\n status: scored.status,\n score: scored.score,\n reasons: scored.reasons,\n worker: worker\n ? {\n status: worker.status,\n score: worker.score,\n reasons: worker.reasons\n }\n : null,\n timestamp: Date.now()\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisRuntime) {\n const payload = await buildGenesisRuntimeSurfacePayloadLive();\n sendJson(res, 200, payload);\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.genesisIngress) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const actor = await resolveMeshActor(req);\n const clientId = actor.ok ? actor.clientUid : (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestGenesisWorldObservationV0(body, { clientId });\n if (!result.ok) {\n const status =\n result.error === \"rate_limited\" || result.error === \"agent_spoke_throttled\"\n ? 429\n : result.error === \"payload_too_large\"\n ? 413\n : 400;\n sendJson(res, status, result);\n return;\n }\n sendJson(res, result.idempotent ? 200 : 202, result);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisStream) {\n const corsOrigin = accessControlAllowOriginValue(req);\n if (!corsOrigin && String(process.env.NODE_ENV || \"\").toLowerCase() === \"production\") {\n const og = String(req.headers?.origin || \"\").trim();\n if (og) {\n console.warn(\"[GATEWAY] genesis SSE: missing Access-Control-Allow-Origin for browser Origin (CORS whitelist mismatch):\", og);\n }\n }\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n res.write(\"retry: 3000\\n\\n\");\n res.write(\": genesis continuity stream\\n\\n\");\n res.write(\"event: genesis\\n\");\n res.write(`data: {\"booted\":true,\"pid\":${process.pid}}\\n\\n`);\n registerGenesisSseClient(res, req);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLatest) {\n const cp = getLatestGenesisSignedCheckpoint();\n if (!cp) {\n sendJson(res, 404, { ok: false, error: \"no_checkpoint_yet\", hint: \"wait_for_seq_interval_or_configure_signing_secret\" });\n return;\n }\n sendJson(res, 200, { ok: true, checkpoint: cp });\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(rhizohRuntime.routes.genesisCheckpointBySeqPrefix)) {\n const tail = pathname.slice(rhizohRuntime.routes.genesisCheckpointBySeqPrefix.length).split(\"/\")[0];\n const seq = parseInt(String(tail || \"\").trim(), 10);\n const out = await genesisCheckpointQueryBySeqV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"checkpoint_not_found\") {\n sendJson(res, 404, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointRange) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryRangeV0(from, to);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"range_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLineage) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const seq = parseInt(String(u.searchParams.get(\"seq\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryLineageV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"lineage_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisContinuityEvents) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const limit = parseInt(String(u.searchParams.get(\"limit\") || \"\").trim(), 10);\n const out = await queryGenesisContinuityEventArchiveV0(from, to, type, limit);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\" || out.error === \"event_archive_disabled\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"archive_file_too_large\") {\n sendJson(res, 413, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplay) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await resolveGenesisReplayRouterV1({\n from: fromSeq,\n to: toSeq,\n type,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayDiff) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const out = await computeGenesisReplayTemporalDiffV1({ from: fromSeq, to: toSeq, type });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEquivalence) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from1 = parseInt(String(u.searchParams.get(\"from1\") || \"\").trim(), 10);\n const to1 = parseInt(String(u.searchParams.get(\"to1\") || \"\").trim(), 10);\n const from2 = parseInt(String(u.searchParams.get(\"from2\") || \"\").trim(), 10);\n const to2 = parseInt(String(u.searchParams.get(\"to2\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await compareGenesisReplayEquivalenceV1({\n from1,\n to1,\n from2,\n to2,\n type,\n includeCheckpoints\n });\n if (\n !out.ok &&\n (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")\n ) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayAnalytics) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayAnalyticsV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEvolution) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const collapseWindows = parseInt(String(u.searchParams.get(\"collapseWindows\") || \"0\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayEvolutionV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n collapseWindows,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/metrics\") {\n res.writeHead(200, { \"Content-Type\": \"text/plain; version=0.0.4; charset=utf-8\" });\n res.end(renderInfraMetricsProm());\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.infraPrometheusMetrics) {\n if (process.env.CASTLE_PROMETHEUS_METRICS !== \"1\") {\n sendJson(res, 404, { ok: false, error: \"prometheus_metrics_disabled\" });\n return;\n }\n res.writeHead(200, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(renderRhizohPrometheusMetrics());\n return;\n }\n if (req.method === \"GET\" && pathname.startsWith(\"/infra/traces/\")) {\n const traceId = decodeURIComponent(pathname.slice(\"/infra/traces/\".length));\n const item = await getTraceById(traceId);\n if (!item) return sendJson(res, 404, { ok: false, error: \"trace_not_found\" });\n return sendJson(res, 200, { ok: true, trace: item });\n }\n if (req.method === \"GET\" && pathname === \"/infra/traces\") {\n const u = new URL(String(req.url || \"/infra/traces\"), `http://${req.headers.host || \"localhost\"}`);\n const sessionId = String(u.searchParams.get(\"sessionId\") || \"\").trim();\n const items = sessionId ? await listTracesBySession(sessionId) : [];\n return sendJson(res, 200, { ok: true, sessionId, traces: items });\n }\n\n /** Presence room mesh (C): JOIN / LEAVE / SNAPSHOT / DELTA / REPLAY (HTTP); SUBSCRIBE = SSE stream. */\n if (req.method === \"POST\" && pathname === \"/presence/mesh/join\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid, clientUid: actor.clientUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/leave\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshLeave(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/snapshot\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n sendJson(res, 200, meshSnapshot(roomUid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/delta\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, MAX_MESSAGE_BYTES);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const seq = meshAppendDelta(roomUid, actor.clientUid, {\n node: body?.node,\n projectionPatch: body?.projectionPatch,\n writerSubject: body?.writerSubject\n });\n sendJson(res, 200, { ok: true, roomUid, seq });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/replay\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const fromNodeId = body?.fromNodeId != null ? String(body.fromNodeId) : \"\";\n const fromSeq = body?.fromSeq;\n if (fromNodeId) {\n sendJson(res, 200, meshReplayFromNodeId(roomUid, fromNodeId));\n return;\n }\n sendJson(res, 200, meshReplayFromSeq(roomUid, fromSeq));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/presence/mesh/subscribe\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const roomUid = String(q.searchParams.get(\"roomUid\") || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n const write = (chunk) => {\n try {\n res.write(chunk);\n } catch {\n /* closed */\n }\n };\n const unsub = meshSubscribeSse(roomUid, write);\n const onClose = () => {\n unsub();\n meshLeave(roomUid, actor.clientUid);\n try {\n res.end();\n } catch {\n /* ignore */\n }\n };\n req.on(\"close\", onClose);\n req.on(\"aborted\", onClose);\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, uid: auth.uid, mode: auth.mode, items: await listConnections(auth.uid) });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const id = await createConnection(auth.uid, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"create_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n await updateConnection(auth.uid, id, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"update_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"DELETE\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n await deleteConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"delete_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections/test\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n if (!checkHttpRateLimit(`llm_conn_test:${auth.uid}`, RL_LLM_CONN_TEST_PER_MIN, 60_000)) {\n return sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n }\n const hasBodyKey = !!String(payload?.apiKey || \"\").trim();\n const result = await queryRhizohLlm(\n {\n message: String(payload?.message || \"Ping from Castle Gateway\"),\n context: payload?.context || { source: \"connection_test\" },\n provider: payload?.provider,\n model: payload?.model,\n apiKey: payload?.apiKey || \"\"\n },\n { allowExternalApiKey: true, llmKeySource: hasBodyKey ? \"auto\" : \"env\" }\n );\n logLlmAccess({\n route: \"/llm/connections/test\",\n uid: auth.uid,\n llmKeyBillingOwner: result.llmKeyBillingOwner,\n llmKeyOrigin: result.llmKeyOrigin,\n provider: result.provider,\n model: result.model,\n connectionId: null\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"connection_test_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAgentIdentities(auth.uid);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerAgentIdentity({\n ownerUid: auth.uid,\n agentId: payload?.agentId,\n role: payload?.role,\n personaSeed: payload?.personaSeed || {},\n capabilityLevel: payload?.capabilityLevel\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/agents/identities/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const owns = await getAgentIdentity(id);\n if (!owns || owns.ownerUid !== auth.uid) return sendJson(res, 403, { ok: false, error: \"forbidden\" });\n const payload = await readHttpJson(req);\n const row = await updateAgentIdentity(id, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/academy/events\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/academy/events\"}`);\n const status = String(url.searchParams.get(\"status\") || \"\");\n const items = await listAcademyEvents({ ownerUid: auth.uid, status, limit: 120 });\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/academy/events\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const ev = await queueAcademyEvent({\n type: payload?.type,\n roomId: payload?.roomId,\n ownerUid: auth.uid,\n participants: payload?.participants || [],\n topic: payload?.topic || \"academy\",\n payload: payload?.payload || {}\n });\n sendJson(res, 200, { ok: true, event: ev });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && String(req.url || \"\").startsWith(\"/academy/events/\") && String(req.url || \"\").endsWith(\"/resolve\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const parts = String(req.url || \"\").split(\"/\").filter(Boolean);\n const eventId = parts[2] || \"\";\n try {\n const payload = await readHttpJson(req);\n const out = await resolveAcademyEvent(eventId, payload || {});\n sendJson(res, 200, { ok: true, event: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_resolve_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/memory\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/memory\"}`);\n const scope = url.searchParams.get(\"scope\") === \"agents\" ? \"agents\" : \"users\";\n const id = String(url.searchParams.get(\"id\") || (scope === \"agents\" ? \"\" : auth.uid));\n const kind = String(url.searchParams.get(\"kind\") || \"\");\n const limit = Number(url.searchParams.get(\"limit\") || 40);\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n sendJson(res, 200, { ok: true, scope, id, kind, items: await listMemories({ scope, id, kind, limit }) });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const profile = await getPersonaGoalMemory(auth.uid);\n sendJson(res, 200, { ok: true, profile });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const profile = await setPersonaGoalMemory({\n uid: auth.uid,\n personaPatch: payload?.personaPatch || {},\n goals: Array.isArray(payload?.goals) ? payload.goals : null,\n preferencesPatch: payload?.preferencesPatch || {}\n });\n sendJson(res, 200, { ok: true, profile });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"profile_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/compact\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const out = await autoCompactMemories({ scope, id });\n sendJson(res, 200, { ok: true, scope, id, ...out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_compact_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/context\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const context = await getMemoryContext({\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n query: String(payload?.query || \"\"),\n limit: Number(payload?.limit || 80)\n });\n sendJson(res, 200, { ok: true, context });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_context_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/place-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const placeName = String(payload?.placeName || \"\").trim();\n if (!placeName) return sendJson(res, 400, { ok: false, error: \"placeName_required\" });\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const persona = String(payload?.waypointPersona || payload?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(placeName)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Place: ${placeName}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Give a concise conversational briefing about this place for a walking-route companion.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-place-brief\", placeName, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `PLACE_BRIEF:${placeName} => ${String(summary.reply || \"\").slice(0, 900)}`,\n tags: [\"place\", \"brief\", \"event-layer\"],\n importance: 0.62,\n meta: { placeName }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"place-brief\",\n text: `${placeName} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: {\n placeName,\n directive: summary.directive,\n lat: hit?.lat ? Number(hit.lat) : null,\n lon: hit?.lon ? Number(hit.lon) : null\n }\n });\n sendJson(res, 200, { ok: true, place: hit || null, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"place_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/route-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const waypoints = Array.isArray(payload?.waypoints) ? payload.waypoints.slice(0, 24) : [];\n const out = [];\n for (const wp of waypoints) {\n const name = String(wp?.name || \"\").trim();\n if (!name) continue;\n const persona = String(wp?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(name)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Waypoint: ${name}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Generate a concise spoken briefing for route waypoint.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-route-brief\", waypoint: name, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n out.push({\n name,\n persona,\n place: hit || null,\n reply: summary.reply,\n directive: summary.directive\n });\n }\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `ROUTE_BRIEF: waypoints=${out.length} ${out.map((x) => `${x.name}:${x.persona}`).join(\" | \")}`.slice(0, 1300),\n tags: [\"route\", \"waypoint\", \"brief\", \"event-layer\"],\n importance: 0.74,\n meta: { count: out.length }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"route-brief\",\n text: out.map((x) => `${x.name}:${String(x.reply || \"\").slice(0, 140)}`).join(\" | \").slice(0, 1400),\n roomId: \"studio-main\",\n meta: {\n waypointCount: out.length,\n waypoints: out.map((x) => ({\n name: x.name,\n persona: x.persona,\n lat: x.place?.lat ? Number(x.place.lat) : null,\n lon: x.place?.lon ? Number(x.place.lon) : null\n }))\n }\n });\n sendJson(res, 200, { ok: true, items: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"route_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/pdf-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const title = String(payload?.title || \"document\");\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const providedText = String(payload?.text || \"\");\n const url = String(payload?.url || \"\");\n let content = providedText;\n if (!content && url) content = await fetchTextFromUrl(url);\n if (!content) return sendJson(res, 400, { ok: false, error: \"text_or_supported_url_required\" });\n const summary = await queryRhizohLlm(\n {\n message: `Summarize this document and provide key points + practical actions.\\nTitle:${title}\\nContent:\\n${content.slice(0, 12000)}`,\n context: { source: \"event-layer-pdf-brief\", title },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `DOC_BRIEF:${title} => ${String(summary.reply || \"\").slice(0, 1000)}`,\n tags: [\"document\", \"brief\", \"library\", \"event-layer\"],\n importance: 0.7,\n meta: { title, url: url || null }\n });\n await addTranscript(auth.uid, {\n source: \"library-brief\",\n eventType: \"pdf-brief\",\n text: `${title} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { title }\n });\n sendJson(res, 200, { ok: true, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/pdf/upload\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req, 8 * 1024 * 1024);\n const job = await enqueuePdfJob({\n uid: auth.uid,\n fileName: payload?.fileName || \"document.pdf\",\n bytesBase64: payload?.contentBase64\n });\n sendJson(res, 200, { ok: true, job });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_upload_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/session\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createPublishSession(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session\",\n text: `Publish session armed: ${row.protocol} ${row.target}`.slice(0, 500),\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: row.id, bridge: row.bridge }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/studio/publish/sessions\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listPublishSessions(auth.uid, 80);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/health-check\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const protocol = String(payload?.protocol || \"WHIP\");\n const target = String(payload?.target || \"\");\n const health = await checkPublishTargetHealth(protocol, target);\n if (payload?.sessionId) {\n await updatePublishSession(auth.uid, String(payload.sessionId), {\n status: health.ok ? \"healthy\" : \"degraded\",\n reconnectPolicy: {\n enabled: true,\n backoffSec: [2, 5, 10, 20],\n nextRetryInSec: health.reconnectInSec || 0,\n maxRetries: 8,\n reason: health.reason\n }\n });\n }\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-health\",\n text: `${protocol} ${target} => ${health.ok ? \"healthy\" : \"degraded\"} (${health.reason})`,\n roomId: \"studio-main\",\n meta: { protocol, target, reconnect: !!health.shouldReconnect }\n });\n sendJson(res, 200, { ok: true, health });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_health_check_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/studio/publish/sessions/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updatePublishSession(auth.uid, id, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session-update\",\n text: `Session ${id} -> ${row.status || \"updated\"}`,\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: id, status: row.status, ingestUrl: row.ingestUrl || \"\" }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/transcripts\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/transcripts\"}`);\n const limit = Math.max(1, Math.min(200, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await listTranscripts(auth.uid, limit);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/transcripts\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await addTranscript(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"transcript_write_failed\" });\n }\n return;\n }\n\n /** E1: broadcast intent → GreenRoom capability → transcript + memory artifact */\n if (req.method === \"POST\" && req.url === \"/studio/capabilities/greenroom\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const traceId = String(payload?.traceId || `TRC-${Date.now().toString(36).toUpperCase()}`);\n const title = String(payload?.title || \"Castle Live Broadcast\").slice(0, 200);\n const intentRaw = String(payload?.intentRaw || \"\").slice(0, 800);\n const audienceEstimate = Math.max(0, Math.min(50_000_000, Number(payload?.audienceEstimate) || 1200));\n const roomId = String(payload?.roomId || \"greenroom-main\");\n const transcriptRow = await addTranscript(auth.uid, {\n source: \"rhizoh\",\n eventType: \"BROADCAST_ROUTED\",\n text: `GreenRoom · ${title}${intentRaw ? `\\nIntent: ${intentRaw}` : \"\"}`,\n roomId,\n meta: {\n ack: \"BROADCAST_ROUTED\",\n capability: \"greenroom\",\n intentType: \"broadcast\",\n traceId,\n audienceEstimate,\n artifact: {\n kind: \"broadcast_card\",\n title,\n traceId,\n roomId,\n createdAt: Date.now()\n }\n }\n });\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n text: `GREENROOM_BROADCAST:${title} trace=${traceId}`,\n tags: [\"broadcast\", \"greenroom\", \"artifact\", \"castle-library\"],\n importance: 0.72,\n kind: \"episodic\",\n meta: { traceId, capability: \"greenroom\", transcriptId: transcriptRow.id }\n });\n sendJson(res, 200, {\n ok: true,\n ack: \"BROADCAST_ROUTED\",\n traceId,\n transcript: transcriptRow,\n replayPath: `/replay/${encodeURIComponent(traceId)}`,\n sharePath: `/replay/${encodeURIComponent(traceId)}`\n });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"greenroom_capability_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/metadata/search\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/metadata/search\"}`);\n const q = String(url.searchParams.get(\"q\") || \"\");\n const limit = Math.max(1, Math.min(300, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await searchMetadata(auth.uid, q, limit);\n sendJson(res, 200, { ok: true, q, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerDevice(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, device: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"device_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const lat = Number(payload?.lat);\n const lon = Number(payload?.lon);\n const speed = Math.max(0, Number(payload?.speed || 0));\n const cfgCenterLat = Number(process.env.CASTLE_GEOFENCE_CENTER_LAT || 41.0082);\n const cfgCenterLon = Number(process.env.CASTLE_GEOFENCE_CENTER_LON || 28.9784);\n const cfgRadiusM = Math.max(100, Number(process.env.CASTLE_GEOFENCE_RADIUS_M || 55000));\n const cfgSpeedCap = Math.max(1, Number(process.env.CASTLE_SPEED_CAP_MPS || 35));\n const dist = Number.isFinite(lat) && Number.isFinite(lon) ? haversineMeters(cfgCenterLat, cfgCenterLon, lat, lon) : 0;\n const geofenceOk = !Number.isFinite(dist) ? true : dist <= cfgRadiusM;\n const speedOk = speed <= cfgSpeedCap;\n const failsafe = !(geofenceOk && speedOk);\n const safety = {\n geofenceOk,\n speedOk,\n failsafe,\n distanceFromCenterM: Number.isFinite(dist) ? dist : null,\n speedCapMps: cfgSpeedCap\n };\n const row = await appendTelemetry(auth.uid, deviceId, {\n lat,\n lon,\n speed,\n battery: payload?.battery,\n mode: payload?.mode || (failsafe ? \"FAILSAFE_HOLD\" : \"ACTIVE\"),\n raw: payload?.raw || {}\n });\n await updateDevice(auth.uid, deviceId, {\n status: failsafe ? \"failsafe\" : \"active\",\n lastTelemetryAt: row.ts,\n safetyState: safety\n });\n if (failsafe) {\n await addTranscript(auth.uid, {\n source: \"robotics-guardrails\",\n eventType: \"failsafe\",\n text: `Device ${deviceId} failsafe triggered geofenceOk=${geofenceOk} speedOk=${speedOk}`,\n roomId: \"studio-main\",\n meta: { deviceId, safety }\n });\n }\n sendJson(res, 200, { ok: true, telemetry: row, safety, command: failsafe ? \"FAILSAFE_HOLD\" : \"CONTINUE\" });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"telemetry_ingest_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listTelemetry(auth.uid, 150);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createAutomation(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"automation-scheduled\",\n text: `Automation scheduled: ${row.title} at ${new Date(row.scheduleAt).toLocaleString()}`,\n roomId: \"studio-main\",\n meta: { automationId: row.id, action: row.action, deviceId: row.deviceId }\n });\n sendJson(res, 200, { ok: true, automation: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"automation_create_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAutomations(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listDevices(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const devs = await listDevices(auth.uid, 500);\n const d = devs.find((x) => x.id === deviceId);\n if (!d) return sendJson(res, 404, { ok: false, error: \"device_not_found\" });\n const action = String(payload?.action || \"\");\n const caps = Array.isArray(d.capabilityProfile) ? d.capabilityProfile : [];\n if (action && caps.length && !caps.includes(action) && !caps.includes(\"*\")) {\n return sendJson(res, 400, { ok: false, error: \"unsupported_action_for_device\", capabilityProfile: caps });\n }\n const cmd = await enqueueCommand(auth.uid, {\n deviceId,\n action: action || \"NOOP\",\n params: payload?.params || {},\n adapter: payload?.adapter || d.adapter || \"websocket\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-command-queue\",\n eventType: \"command-queued\",\n text: `${cmd.deviceId} -> ${cmd.action}`,\n roomId: \"studio-main\",\n meta: { commandId: cmd.id, adapter: cmd.adapter }\n });\n sendJson(res, 200, { ok: true, command: cmd });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listCommands(auth.uid, 200);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/robotics/commands/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updateCommandStatus(auth.uid, id, payload?.status || \"sent\");\n sendJson(res, 200, { ok: true, command: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/rhizoh/bridge\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const result = await runRhizohBrainV2({\n message: String(payload?.message || \"robotics bridge command\"),\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n worldState: payload?.worldState || {},\n deviceState: payload?.deviceState || {},\n intent: \"robotics-mechanics\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-bridge\",\n eventType: \"robotics-command\",\n text: `Bridge -> ${String(result.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { directive: result.directive, agentId: payload?.agentId || \"\" }\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"robotics_bridge_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/planner/graph\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const graph = buildTaskActionGraph(String(payload?.task || \"\"), payload?.context || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"planner-graph\",\n text: `Planner graph built: ${graph.taskTitle} nodes=${graph.nodes.length}`,\n roomId: \"studio-main\",\n meta: { taskTitle: graph.taskTitle, nodeCount: graph.nodes.length }\n });\n sendJson(res, 200, { ok: true, graph });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"planner_graph_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerSocialChannel(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, channel: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_channel_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listSocialChannels(auth.uid, 100);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/broadcast\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const text = String(payload?.text || \"\").slice(0, 1200);\n const channels = await listSocialChannels(auth.uid, 100);\n const enabled = channels.filter((c) => c.status === \"enabled\");\n const deliveries = [];\n for (const c of enabled) {\n const out = await deliverSocialWithRetry(c, text);\n deliveries.push({ id: c.id, platform: c.platform, ...out });\n }\n const okCount = deliveries.filter((d) => d.ok).length;\n await addTranscript(auth.uid, {\n source: \"social-layer\",\n eventType: \"social-broadcast\",\n text: `Broadcast delivered ${okCount}/${enabled.length} channel(s): ${text.slice(0, 240)}`,\n roomId: \"studio-main\",\n meta: { deliveries }\n });\n sendJson(res, 200, { ok: true, sentChannels: okCount, totalChannels: enabled.length, deliveries });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_broadcast_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/ethics/programs\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, ...buildEthicsPrograms() });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands/dispatch\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const all = await listCommands(auth.uid, 200);\n const queued = all.filter((c) => String(c.status || \"queued\") === \"queued\").slice(0, 50);\n const devices = await listDevices(auth.uid, 500);\n const byId = new Map(devices.map((d) => [d.id, d]));\n const results = [];\n for (const cmd of queued) {\n const dev = byId.get(cmd.deviceId);\n if (!dev) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: \"device_not_found\" });\n continue;\n }\n try {\n const dispatched = await dispatchCommandToAdapter(dev, cmd);\n await updateCommandStatus(auth.uid, cmd.id, \"sent\");\n results.push({ id: cmd.id, ok: true, adapter: dispatched.adapter, via: dispatched.via });\n } catch (error) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: String(error?.message || \"dispatch_failed\") });\n }\n }\n await addTranscript(auth.uid, {\n source: \"robotics-command-dispatch\",\n eventType: \"command-dispatch-batch\",\n text: `Dispatch batch size=${results.length} ok=${results.filter((x) => x.ok).length}`,\n roomId: \"studio-main\",\n meta: { results: results.slice(0, 30) }\n });\n sendJson(res, 200, { ok: true, processed: results.length, results });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"dispatch_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/pdf/jobs/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n const job = await getPdfJob(auth.uid, id);\n if (!job) return sendJson(res, 404, { ok: false, error: \"pdf_job_not_found\" });\n sendJson(res, 200, { ok: true, job });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const row = await appendMemory({\n scope,\n id,\n text: payload?.text,\n tags: payload?.tags || [],\n importance: payload?.importance,\n kind: payload?.kind || \"episodic\",\n meta: { uid: auth.uid, ...(payload?.meta || {}) }\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.externalTruth) {\n try {\n if (process.env.RHIZOH_EXTERNAL_TRUTH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_truth:${ip}`, RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n sendJson(res, 200, buildRhizohExternalGroundTruthPayload(req));\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.productOutcome) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const rawBody = await readHttpBodyText(req, 64 * 1024);\n const body = rawBody.trim() ? safeJsonParse(rawBody) : null;\n if (!body || typeof body !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"invalid_json_payload\" });\n return;\n }\n const sigHdr = String(req.headers?.[\"x-rhizoh-outcome-signature\"] || \"\").trim();\n const hmacV = verifyRhizohOutcomeHmac(rawBody, sigHdr);\n if (!hmacV.ok) {\n sendJson(res, 401, { ok: false, error: hmacV.error || \"outcome_hmac_invalid\" });\n return;\n }\n const srcTokHdr = String(req.headers?.[\"x-rhizoh-outcome-source-token\"] || \"\").trim();\n const srcV = verifyRhizohOutcomeSourceToken(body.source, srcTokHdr);\n if (!srcV.ok) {\n sendJson(res, 401, { ok: false, error: srcV.error || \"outcome_source_token_invalid\" });\n return;\n }\n const subjectKey = String(body.subjectKey || \"\").trim().slice(0, 128);\n if (\n subjectKey &&\n !checkHttpRateLimit(\n `rhizoh_product_outcome_subj:${subjectKey}`,\n RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN,\n 60_000\n )\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_subject\" });\n return;\n }\n const result = ingestRhizohProductOutcomeHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n schemaVersion: \"1.1.0\",\n aggregate: result.aggregate,\n integrity: result.integrity\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"outcome_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.productOutcomeAggregate) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome_agg:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const u = new URL(String(req.url || \"/\"), \"http://localhost\");\n const cohortId = u.searchParams.get(\"cohortId\") || \"\";\n const decisionFingerprint = u.searchParams.get(\"decisionFingerprint\") || \"\";\n const limit = u.searchParams.get(\"limit\") || \"50\";\n const rows = listRhizohProductOutcomeAggregates({\n cohortId: cohortId || undefined,\n decisionFingerprint: decisionFingerprint || undefined,\n limit: Number(limit) || 50\n });\n sendJson(res, 200, { ok: true, schemaVersion: \"1.1.0\", rows });\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.externalLossBatch) {\n try {\n if (process.env.RHIZOH_EXTERNAL_LOSS_BATCH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_loss_batch:${ip}`, RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const result = ingestRhizohExternalLossBatchHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, { ok: true, accepted: result.accepted ?? 0 });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"batch_failed\") });\n }\n return;\n }\n\n function moderationAdminOk(req) {\n const expected = String(process.env.CASTLE_MODERATION_ADMIN_KEY || \"\").trim();\n if (!expected) return false;\n const got = String(req.headers[\"x-castle-moderation-key\"] || \"\").trim();\n return got.length > 0 && got === expected;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.opsAbuseReport) {\n try {\n const body = await readHttpJson(req, 32 * 1024);\n const auth = await resolveHttpUser(req);\n const entry = submitAbuseReportV0({\n reporterUid: auth.ok ? auth.uid : null,\n targetUid: body?.targetUid ?? null,\n category: body?.category,\n detail: body?.detail,\n traceId: body?.traceId ?? null\n });\n sendJson(res, 202, { ok: true, reportId: entry.id, status: entry.status });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsModerationQueue) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n queue: listModerationQueueV0(100),\n softBlocked: listSoftBlockedUsersV0()\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-block\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const out = softBlockUserV0(body?.uid, body?.reason);\n sendJson(res, out.ok ? 200 : 400, out);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-unblock\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n sendJson(res, 200, removeSoftBlockV0(body?.uid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsAgentSnapshots) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n const limit = Number(new URL(req.url || \"\", \"http://local\").searchParams.get(\"limit\") || 50);\n sendJson(res, 200, { ok: true, snapshots: listRecentAgentSnapshotsV0(limit) });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.academicObservatoryExport) {\n const auth = await resolveHttpUser(req);\n const out = handleAcademicObservatoryExportGetV0(req, auth);\n sendJson(res, out.status, out.body);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsHardeningStatus) {\n const auth = await resolveHttpUser(req);\n const principal = auth.ok ? `uid:${auth.uid}` : null;\n const gclSnap = await getGlobalCostLedgerSnapshotV0(principal);\n const economicStrategy = evaluateEconomicStrategyL3V0(gclSnap);\n const hardeningTenantId = resolveHardeningTenantIdV0(auth, req);\n const unifiedState = await buildUnifiedStateNarrativeV0({\n tenantId: hardeningTenantId ?? undefined,\n platformScope: !hardeningTenantId,\n principal\n });\n sendJson(res, 200, {\n ok: true,\n schema: \"rhizoh.ops.hardening_status.v0\",\n containment: readAgentContainmentConfigV0(),\n cost: readCostContainmentConfigV0(),\n gcl: {\n backend: await getGclBackendStatusV0(),\n snapshot: gclSnap\n },\n economicStrategyL3: economicStrategy,\n phasedRollout: await getPhasedRolloutStatsV0(),\n tier: readPhasedRolloutTierV0(),\n principalCost: auth.ok ? await getCostContainmentStatsV0(principal) : null,\n stressTaxonomy: verifyStressTaxonomyCoverageV0(),\n stressConflictResolution: verifyStressConflictResolutionV0(),\n resolutionStabilityEnvelope: verifyResolutionStabilityEnvelopeV0(),\n unifiedState: {\n tenantScope: {\n tenantId: unifiedState.tenantScope?.tenantId,\n scope: unifiedState.tenantScope?.scope,\n noGlobalDerivedState: unifiedState.tenantScope?.noGlobalDerivedState\n },\n narrativeFingerprint: unifiedState.narrativeFingerprint,\n screenshotScopeWatermark: unifiedState.screenshotScopeWatermark,\n interpretationSafetyContract: unifiedState.interpretationSafetyContract,\n interpretationUxContract: {\n schema: unifiedState.interpretationUxContract?.schema,\n layerOrder: unifiedState.interpretationUxContract?.layerOrder,\n narrativeNeverVisuallyDominant: unifiedState.interpretationUxContract?.narrativeNeverVisuallyDominant\n },\n governance: {\n narrativeIsNotDecisionLayer: unifiedState.governance.narrativeIsNotDecisionLayer,\n displayOrder: unifiedState.governance.displayOrder,\n trustPosture: unifiedState.governance.trustPosture,\n interpretationUxContract: unifiedState.governance.interpretationUxContract?.schema\n },\n humanOps: {\n decisionTier: unifiedState.humanOps?.humanDecisionScaling?.tier,\n decisionOwner: unifiedState.humanOps?.humanDecisionScaling?.decisionOwner,\n opsRouteId: unifiedState.humanOps?.humanDecisionOpsRunbook?.routing?.routeId,\n slaAckMinutes: unifiedState.humanOps?.humanDecisionOpsRunbook?.sla?.ackMinutes,\n uiDisclaimerRequired: unifiedState.humanOps?.narrativeUiSafety?.displayRules?.requireDisclaimerBanner,\n misreadHighResidual: unifiedState.humanOps?.misreadSimulation?.highResidualCount,\n propagationHighResidual: unifiedState.humanOps?.socialPropagationSimulation?.highResidualCount,\n worstWatermarkSurvivability:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.worstWatermarkSurvivability,\n dominantDistortionSource:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.dominantDistortionSource\n },\n culturalRisk: {\n perceptualRiskClass: unifiedState.culturalRisk?.perceptualRiskClass,\n trustFork: unifiedState.culturalRisk?.trustDynamics?.fork,\n trustDecayScore: unifiedState.culturalRisk?.trustDynamics?.scores?.trustDecay,\n mythologyScore: unifiedState.culturalRisk?.trustDynamics?.scores?.mythology,\n confidenceDetachmentExposure:\n unifiedState.culturalRisk?.trustDynamics?.axes?.confidenceDetachmentExposure\n },\n appliedSystemsLayer: {\n schema: unifiedState.appliedSystemsLayer?.schema,\n researchOutputType: unifiedState.appliedSystemsLayer?.researchGovernance?.outputType,\n roboticsBlockActuation: unifiedState.appliedSystemsLayer?.roboticsGrounding?.blockActuation,\n spiralRumorCount:\n unifiedState.appliedSystemsLayer?.spiralMMOGameKernel?.gameLayer?.rumors?.length\n },\n actionSemanticGovernance: {\n schema: unifiedState.actionSemanticGovernance?.schema,\n highRiskActionCount: unifiedState.actionSemanticGovernance?.highRiskActionCount,\n loopClosed: unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.loopClosed,\n authorityCollapseMitigated:\n unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.risks?.authorityCollapse\n ?.mitigated\n },\n actionContextResolution: {\n schema: unifiedState.actionContextResolution?.schema,\n contextFingerprint: unifiedState.actionContextResolution?.contextFingerprint?.fingerprintHash,\n domainCollision: unifiedState.actionContextResolution?.domainCollision?.detected,\n deployAgentSpiralBlocked:\n unifiedState.actionContextResolution?.actionOverloadingRisk?.deployAgentExample\n ?.spiral_mmo === \"blocked\",\n roboticsActuationEligible:\n unifiedState.actionContextResolution?.roboticsActuationCorrectness?.actuationEligible\n },\n epistemicCoherence: {\n schema: unifiedState.epistemicCoherence?.schema,\n verdict: unifiedState.epistemicCoherence?.systemCoherence?.verdict,\n coherenceScore: unifiedState.epistemicCoherence?.systemCoherence?.score,\n contradictionCount: unifiedState.epistemicCoherence?.crossLayerContradictions?.count,\n operatorDecision: unifiedState.epistemicCoherence?.uxCompression?.operatorDecision,\n headlineTr: unifiedState.epistemicCoherence?.uxCompression?.headline?.tr,\n falseSafetyWarning: Boolean(\n unifiedState.epistemicCoherence?.uxCompression?.falseSafetyWarning\n )\n },\n coherenceAuthorityBoundary: {\n schema: unifiedState.coherenceAuthorityBoundary?.schema,\n bindingValid: unifiedState.coherenceAuthorityBoundary?.eclAuthorityBinding?.valid,\n canExecute: unifiedState.coherenceAuthorityBoundary?.contract?.can_execute,\n mandatoryBannerTr:\n unifiedState.coherenceAuthorityBoundary?.disclaimers?.mandatoryBanner?.tr\n },\n decisionLatencyGovernance: {\n schema: unifiedState.decisionLatencyGovernance?.schema,\n latencyTier: unifiedState.decisionLatencyGovernance?.latencyTier,\n inflationScore: unifiedState.decisionLatencyGovernance?.latencyInflation?.score,\n fastPathEligible: unifiedState.decisionLatencyGovernance?.fastPath?.eligible,\n primaryActionId: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.primaryActionId,\n ackSlaMinutes: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.ackSlaMinutes,\n certaintyCap: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.certaintyCap,\n uncertaintyItems:\n unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.uncertaintyEnvelope?.itemCount\n },\n decisionPacketUncertaintyBoundary: {\n schema: unifiedState.decisionPacketUncertaintyBoundary?.schema,\n invariantValid: unifiedState.decisionPacketUncertaintyBoundary?.invariantCheck?.valid,\n centralInvariant: unifiedState.decisionPacketUncertaintyBoundary?.centralInvariant\n },\n epistemicDecisionPacing: {\n schema: unifiedState.epistemicDecisionPacing?.schema,\n queueSaturationTier:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation?.tier,\n operatorProcessingLatency:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation\n ?.operatorProcessingLatency,\n postureWindowCount:\n unifiedState.epistemicDecisionPacing?.temporaryStaticPostureWindows\n ?.temporaryStaticPostureWindows?.windowCount,\n executionBoundariesValid:\n unifiedState.epistemicDecisionPacing?.executionBoundaries?.valid,\n grantsExecutionApproval: unifiedState.epistemicDecisionPacing?.grantsExecutionApproval\n },\n epistemicTemporalCoherence: {\n schema: unifiedState.epistemicTemporalCoherence?.schema,\n allInvariantsValid: unifiedState.epistemicTemporalCoherence?.allInvariantsValid,\n temporalAlignmentValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.temporalAlignment?.valid,\n crossWindowGuardValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard?.valid,\n delayedTruthValid: unifiedState.epistemicTemporalCoherence?.invariants?.delayedTruthRule?.valid,\n reconciliationRequired:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard\n ?.reconciliation?.required\n },\n realityDriftObserver: {\n schema: unifiedState.realityDriftObserver?.schema,\n driftActive: unifiedState.realityDriftObserver?.driftObserverSummary?.active,\n misapprehensionShapeCount:\n unifiedState.realityDriftObserver?.misapprehensionShapeCatalog?.shapeCount,\n criticalShapeCount:\n unifiedState.realityDriftObserver?.driftObserverSummary?.criticalShapeCount,\n roboticsMismatchActive:\n unifiedState.realityDriftObserver?.roboticsFeedbackMismatch?.active\n },\n driftCausality: {\n schema: unifiedState.driftCausality?.schema,\n shapesWithCausalExplanation: unifiedState.driftCausality?.rdolLinkage?.shapesWithCausalExplanation,\n activeDomainCount: unifiedState.driftCausality?.causalitySummary?.activeDomainCount,\n propagationCausalityActive: unifiedState.driftCausality?.causalDomains?.propagation?.active\n },\n productReadiness: unifiedState.productReadiness,\n stateLayers: {\n raw: unifiedState.stateLayers.raw,\n derived: {\n systemState: unifiedState.stateLayers.derived.systemState,\n narrative: unifiedState.stateLayers.derived.narrative,\n validation: unifiedState.stateLayers.derived.validation\n },\n policy: {\n binding: unifiedState.stateLayers.policy.binding,\n decisionOwner: unifiedState.stateLayers.policy.decisionOwner,\n prohibitedActions: unifiedState.stateLayers.policy.prohibitedActions\n }\n },\n systemState: unifiedState.systemState,\n interpretation: unifiedState.interpretation,\n validation: {\n trustPosture: unifiedState.validation.trustPosture,\n adjustedConfidence: unifiedState.validation.adjustedConfidence,\n agreement: unifiedState.validation.confidenceDecomposition.agreement,\n divergenceScore: unifiedState.validation.divergence.divergenceScore,\n validated: unifiedState.validation.divergence.validated,\n uncertaintyTags: unifiedState.validation.uncertainty.tags.map((t) => t.id),\n flags: unifiedState.validation.divergence.flags.map((f) => f.id)\n },\n compression: unifiedState.compression\n }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3) {\n try {\n const payload = await readHttpJson(req, 6 * 1024 * 1024);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (\n !checkHttpRateLimit(`rhizoh_voice_transcribe:${rlKey}`, RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN, 60_000)\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const voiceEnv = rhizohVoiceTranscribeEnvV3();\n if (!voiceEnv.any) {\n sendJson(res, 503, {\n ok: false,\n error: \"voice_asr_not_configured\",\n env: voiceEnv\n });\n return;\n }\n const result = await runRhizohVoiceTranscribeV3(payload);\n sendJson(res, result.ok ? 200 : 422, {\n ...result,\n traceId: String(payload?.traceId || \"\"),\n atMs: Date.now()\n });\n } catch (error) {\n sendJson(res, 500, {\n ok: false,\n error: \"voice_transcribe_failed\",\n detail: String(error?.message || error)\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === rhizohRuntime.routes.rhizohLlm) {\n try {\n const payload = await readHttpJson(req);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n logRhizohHealth(\"gateway_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: String(payload?.traceId || \"\"),\n auth: auth.ok ? \"ok\" : \"anon\",\n ip\n });\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (!checkHttpRateLimit(`rhizoh_llm:${rlKey}`, RL_RHIZOH_LLM_PER_MIN, 60_000)) {\n const rlTaxonomy = classifyStressResponseV0({ code: \"rate_limit_exceeded\" });\n return sendJson(res, 429, {\n ok: false,\n error: \"rate_limit_exceeded\",\n reply: rlTaxonomy.userMessageTr || \"İstek sınırı aşıldı. Kısa süre sonra tekrar deneyin.\",\n stressClass: rlTaxonomy.stressClass,\n responseAction: rlTaxonomy.responseAction,\n stressMatrix: rlTaxonomy.matrix,\n stressInterpretable: rlTaxonomy.interpretable,\n stressConfidence: rlTaxonomy.stressConfidence,\n actionConfidence: rlTaxonomy.actionConfidence,\n actionSoftened: rlTaxonomy.actionSoftened,\n responseActionStrict: rlTaxonomy.responseActionStrict,\n actionInterpretable: rlTaxonomy.actionInterpretable,\n conflictResolution: rlTaxonomy.conflictResolution,\n directive: \"NONE\"\n });\n }\n\n const requireExplicit = process.env.CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE === \"1\";\n const keyMode = normalizeClientLlmKeySource(payload?.llmKeySource ?? payload?.keySource);\n if (keyMode === null) {\n return sendJson(res, 400, {\n ok: false,\n error: \"invalid_llm_key_source\",\n reply: \"llmKeySource geçersiz. Kullanın: env | user_connection | auto\",\n directive: \"NONE\"\n });\n }\n if (requireExplicit && keyMode === \"auto\") {\n return sendJson(res, 400, {\n ok: false,\n error: \"llm_key_source_required\",\n reply: \"Sunucu CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE=1: istekte llmKeySource (env veya user_connection) zorunlu.\",\n directive: \"NONE\"\n });\n }\n\n const conn = auth.ok ? await resolveConnection(auth.uid, String(payload?.connectionId || \"\")) : null;\n\n if (keyMode === \"user_connection\") {\n if (!auth.ok) {\n return sendJson(res, 401, {\n ok: false,\n error: \"llm_auth_required_for_user_connection\",\n reply: \"Kayıtlı LLM bağlantısı için giriş gerekli.\",\n directive: \"NONE\"\n });\n }\n if (!conn?.apiKey) {\n return sendJson(res, 400, {\n ok: false,\n error: \"user_llm_connection_required\",\n reply: \"Geçerli bir LLM bağlantısı yok; Studio’da bağlantı ekleyin veya llmKeySource=env kullanın.\",\n directive: \"NONE\"\n });\n }\n }\n\n const safePayload = { ...(payload || {}) };\n delete safePayload.apiKey;\n delete safePayload.llmKeySource;\n delete safePayload.keySource;\n\n let resolvedProvider = payload?.provider;\n let resolvedModel = payload?.model;\n let connApiKey = \"\";\n\n if (keyMode === \"user_connection\") {\n resolvedProvider = conn.provider;\n resolvedModel = conn.model;\n connApiKey = conn.apiKey || \"\";\n } else if (keyMode === \"env\") {\n connApiKey = \"\";\n } else {\n resolvedProvider = conn?.provider || payload?.provider;\n resolvedModel = conn?.model || payload?.model;\n connApiKey = conn?.apiKey || \"\";\n }\n\n const { result, traceId, turnLatencyMs, spinePhases, sampledTrace } = await rhizohGatewayTurn({\n safePayload,\n auth,\n keyMode,\n conn,\n resolvedProvider,\n resolvedModel,\n connApiKey\n });\n logRhizohHealth(\"llm_response\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId,\n turnLatencyMs: Number(turnLatencyMs || 0),\n keyMode\n });\n sendJson(res, 200, {\n ...result,\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n traceId,\n turnLatencyMs,\n ...(spinePhases ? { spinePhases } : {}),\n ...(sampledTrace !== undefined ? { sampledTrace } : {})\n });\n } catch (error) {\n const msg = String(error?.message || \"\");\n const code = error?.code || \"\";\n let status = 500;\n if (msg === \"rate_limit_exceeded\") status = 429;\n else if (\n code === \"cost_hard_limit\" ||\n code === \"phased_rollout_capacity\" ||\n error?.containment === true ||\n String(code).startsWith(\"agent_\")\n ) {\n status = 429;\n } else if (code === \"user_soft_blocked\" || code === \"prompt_abuse_detected\") status = 403;\n else if ([\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(code) || msg.includes(\"missing_api_key_for_\"))\n status = 400;\n let rhizohFailureKind = \"provider_error\";\n if (msg === \"rate_limit_exceeded\" || status === 429) rhizohFailureKind = \"rate_limit\";\n else if (status === 403) rhizohFailureKind = \"policy_block\";\n else if (status === 400) rhizohFailureKind = \"client_config\";\n else if (msg.startsWith(\"provider_http_\")) {\n const st = Number(msg.replace(\"provider_http_\", \"\"));\n if (st === 408 || st === 504) rhizohFailureKind = \"timeout\";\n else rhizohFailureKind = \"provider_error\";\n } else if (/timeout|timed out|ETIMEDOUT/i.test(msg)) rhizohFailureKind = \"timeout\";\n\n const providerHttpMatch = msg.match(/^provider_http_(\\d{3})$/);\n const stressTaxonomy =\n error?.stressClass != null\n ? {\n stressClass: error.stressClass,\n responseAction: error.responseAction,\n stressMatrix: error.stressMatrix,\n stressInterpretable: error.stressInterpretable !== false,\n stressConfidence: error.stressConfidence,\n actionConfidence: error.actionConfidence,\n actionSoftened: error.actionSoftened,\n responseActionStrict: error.responseActionStrict,\n actionInterpretable: error.actionInterpretable,\n conflictResolution: error.conflictResolution,\n stressSecondary: error.stressSecondary,\n responseActions: error.responseActions\n }\n : (() => {\n const t = classifyStressResponseV0({\n code: code || msg,\n rhizohFailureKind,\n providerHttpStatus: providerHttpMatch\n ? Number(providerHttpMatch[1])\n : null\n });\n return {\n stressClass: t.stressClass,\n responseAction: t.responseAction,\n stressMatrix: t.matrix,\n stressInterpretable: t.interpretable,\n stressConfidence: t.stressConfidence,\n actionConfidence: t.actionConfidence,\n actionSoftened: t.actionSoftened,\n responseActionStrict: t.responseActionStrict,\n actionInterpretable: t.actionInterpretable,\n conflictResolution: t.conflictResolution,\n stressSecondary: t.stressSecondary,\n responseActions: t.responseActions\n };\n })();\n sendJson(res, status, {\n ok: false,\n rhizohFailureKind,\n ...(providerHttpMatch ? { providerHttpStatus: Number(providerHttpMatch[1]) } : {}),\n error: code || msg || \"rhizoh_llm_failed\",\n reply:\n error?.reply ||\n (status === 500 ? \"Rhizoh bağlantısı geçici olarak kesildi.\" : msg),\n directive: error?.directive || \"NONE\",\n ...stressTaxonomy\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicSeal) {\n try {\n if (!EPISTEMIC_SEAL_SECRET) {\n sendJson(res, 503, { ok: false, error: \"epistemic_seal_disabled\" });\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`epistemic_seal:${ip}`, RL_EPISTEMIC_SEAL_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const truth = body.truth_contract;\n if (!truth || typeof truth !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"truth_contract_required\" });\n return;\n }\n const sealBody = {\n truth_contract: truth,\n runtime_hash: String(body.runtime_hash || \"\"),\n model_route: {\n provider: body.model_route?.provider ?? null,\n model: body.model_route?.model ?? null\n },\n memory_digest: String(body.memory_digest || \"\"),\n world_snapshot_hash: String(body.world_snapshot_hash || \"\"),\n timestamp: Number(body.timestamp) || Date.now()\n };\n const canonical = canonicalEpistemicSealString(sealBody);\n const { hash, signature } = hashAndSignEpistemicSeal(canonical, EPISTEMIC_SEAL_SECRET);\n recordGenesisEpistemicSealIssued(hash);\n const attestation = {\n schema: EPISTEMIC_SEAL_SCHEMA,\n algorithm: \"SHA-256+HMAC-SHA256\",\n keyHint: process.env.CASTLE_EPISTEMIC_SEAL_SECRET ? \"CASTLE_EPISTEMIC_SEAL_SECRET\" : \"CASTLE_GATEWAY_TOKEN\",\n issuedAt: Date.now(),\n gateway: { service: \"castle-gateway\", listenPort: PORT }\n };\n sendJson(res, 200, {\n ok: true,\n hash,\n signature,\n attestation,\n canonicalBytes: Buffer.byteLength(canonical, \"utf8\")\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"seal_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicLogsBatch) {\n try {\n const auth = await resolveEpistemicIngestActor(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`epistemic_logs:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 120) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const persisted = await persistEpistemicLedgerBatch(auth.uid, entries);\n recordGenesisEpistemicLedgerPersisted(persisted.persisted);\n // Internal cognition layer: forecast stays gateway/ledger-only (no client exposure).\n try {\n await persistEpistemicForecastBatch(auth.uid, persisted.normalized || []);\n } catch {\n /* forecast failures must not break primary ledger ingest */\n }\n sendJson(res, 200, {\n ok: true,\n persisted: persisted.persisted,\n mode: persisted.mode,\n latest: persisted.normalized?.[persisted.normalized.length - 1] || null\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"epistemic_log_ingest_failed\") });\n }\n return;\n }\n\n sendJson(res, 404, { ok: false, error: \"not_found\" });\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\nspiralWssForGenesis = wss;\nconst orchestrator = createOrchestrator();\nconst clientStats = new Map();\nlet broadcasterClientId = null;\nconst spiralState = {\n rooms: [{ id: \"room-default\", name: \"Spiral Default Arena\" }],\n activeByClient: new Map(),\n characters: new Map() // id -> {id,name,role,roomId,source}\n};\n\n/** Castle social rooms — keyed by `castleRoomKey` from {@link WS_MESSAGE.CASTLE_SOCIAL_PULSE}. */\nconst castleSocialRoomState = new Map();\nconst CASTLE_SOCIAL_MEMBER_TTL_MS = 90_000;\nconst CASTLE_SOCIAL_MAX_PULSE_BYTES = 8192;\n\nfunction removeCastleSocialClientFromAllRooms(clientId) {\n const cid = String(clientId || \"\");\n if (!cid) return;\n for (const room of castleSocialRoomState.values()) {\n room.members.delete(cid);\n }\n}\n\nfunction buildCastleSocialRoomPayload(roomKey) {\n const room = castleSocialRoomState.get(roomKey);\n if (!room) return null;\n const now = Date.now();\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const roster = [...room.members.values()].map((m) => ({\n userId: m.userId,\n lastMs: m.lastMs,\n ...m.sp\n }));\n const peerFeeds =\n room.walPeers && room.walPeers.size\n ? [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }))\n : [];\n return { castleRoomKey: roomKey, seq: room.seq, roster, peerFeeds, ts: now };\n}\n\nconst CASTLE_WAL_PEER_MAX_FEED_BYTES = 48 * 1024;\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {{ ok: boolean, code?: string, reason?: string }} gate\n */\nfunction rejectWalPeerIngressV0(socket, gate) {\n const code = gate.code || \"wal_peer_rejected\";\n recordWalPeerRejectV0(code);\n recordSubstrateOtelEventV0(\"wal_peer_reject\", { code, reason: String(gate.reason || \"\").slice(0, 120) });\n socket.send(\n JSON.stringify(\n createEnvelope(WS_MESSAGE.ERROR, {\n error: gate.reason || \"WAL peer ingress rejected.\",\n code\n })\n )\n );\n}\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {Record<string, unknown> | null | undefined} walPeerFeed\n * @returns {boolean}\n */\nfunction gateWalPeerIngressV0(socket, walPeerFeed) {\n const authGate = assertWalPeerSocketAuthorityV0(socket);\n if (!authGate.ok) {\n rejectWalPeerIngressV0(socket, authGate);\n return false;\n }\n const sigGate = validateWalPeerFeedSignatureV0(\n walPeerFeed && typeof walPeerFeed === \"object\" ? walPeerFeed : {}\n );\n if (!sigGate.ok) {\n rejectWalPeerIngressV0(socket, sigGate);\n return false;\n }\n return true;\n}\n\nfunction handleCastleWalPeerFeed(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_WAL_PEER_MAX_FEED_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_WAL_PEER_FEED too large.\" })));\n return;\n }\n const walPeerFeed = p.walPeerFeed && typeof p.walPeerFeed === \"object\" ? p.walPeerFeed : {};\n if (!gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const castleRoomKey = String(p.castleRoomKey || \"wal:convergence\").slice(0, 64);\n const castleId = String(p.castleId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId,\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: room.members.get(socket.clientId)?.sp ?? {}\n });\n const peerFeeds = [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }));\n const body = {\n castleRoomKey,\n seq: room.seq,\n peerFeeds,\n ts: now\n };\n const env = createEnvelope(WS_MESSAGE.CASTLE_WAL_PEER_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys(), ...room.walPeers.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction handleCastleSocialPulse(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_SOCIAL_MAX_PULSE_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_SOCIAL_PULSE too large.\" })));\n return;\n }\n const castleRoomKey = String(p.castleRoomKey || \"default\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n const sp = p.socialPulse && typeof p.socialPulse === \"object\" ? p.socialPulse : {};\n const walPeerFeed =\n p.walPeerFeed && typeof p.walPeerFeed === \"object\"\n ? p.walPeerFeed\n : sp.walPeerFeed && typeof sp.walPeerFeed === \"object\"\n ? sp.walPeerFeed\n : null;\n if (walPeerFeed && !gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const spNext = walPeerFeed ? { ...sp, walPeerFeed } : { ...sp };\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: spNext\n });\n if (walPeerFeed) {\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId: String(p.castleId || userId).slice(0, 64),\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n }\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const body = buildCastleSocialRoomPayload(castleRoomKey);\n if (!body) return;\n const env = createEnvelope(WS_MESSAGE.CASTLE_SOCIAL_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcast(msgObj) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcastState() {\n broadcast(\n createEnvelope(WS_MESSAGE.BROADCAST_STATE, {\n broadcasterClientId,\n viewers: [...wss.clients].filter((c) => c.readyState === 1).length\n })\n );\n}\n\nfunction currentSpiralStatePayload() {\n return {\n rooms: spiralState.rooms,\n characters: [...spiralState.characters.values()]\n };\n}\n\nfunction broadcastSpiralState() {\n broadcast(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload()));\n}\n\nfunction ensureRoom(roomId, fallbackName = null) {\n if (!roomId) return spiralState.rooms[0].id;\n const exists = spiralState.rooms.find((r) => r.id === roomId);\n if (exists) return exists.id;\n const created = { id: roomId, name: fallbackName || `Spiral-${roomId.slice(-4)}` };\n spiralState.rooms.push(created);\n return created.id;\n}\n\nfunction syncSpiralCharactersWithWorld(snapshot) {\n const agents = snapshot?.agents || [];\n for (const a of agents) {\n if (!spiralState.characters.has(a.id)) {\n spiralState.characters.set(a.id, {\n id: a.id,\n name: a.id,\n role: \"agent\",\n roomId: spiralState.rooms[0].id,\n source: \"world\"\n });\n }\n }\n}\n\nfunction broadcastExcept(msgObj, exceptSocket) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client !== exceptSocket && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction validateInputFrame(payload, socket) {\n const stats = clientStats.get(socket.clientId) || { lastFrame: 0, windowStart: Date.now(), msgCount: 0 };\n const now = Date.now();\n if (now - stats.windowStart > 5000) {\n stats.windowStart = now;\n stats.msgCount = 0;\n }\n stats.msgCount += 1;\n clientStats.set(socket.clientId, stats);\n if (stats.msgCount > 120) return \"Rate limit exceeded.\";\n\n if (!payload || !Number.isInteger(payload.frame)) return \"frame must be integer.\";\n if (payload.frame <= stats.lastFrame) return \"frame must be monotonic.\";\n const commands = Array.isArray(payload.commands) ? payload.commands : [];\n if (commands.length > 8) return \"Too many commands in one frame.\";\n for (const cmd of commands) {\n if (!cmd || typeof cmd.command !== \"string\") return \"Invalid command format.\";\n if (\n cmd.command !== \"SPAWN_AGENT\" &&\n cmd.command !== \"LIST_CASTLES\" &&\n cmd.command !== \"SPAWN_ENTITY\" &&\n cmd.command !== \"CREATE_CASTLE\"\n ) return \"Unsupported command.\";\n }\n stats.lastFrame = payload.frame;\n clientStats.set(socket.clientId, stats);\n return null;\n}\n\nfunction validateSignalPayload(payload) {\n if (!payload || typeof payload !== \"object\") return \"Invalid signaling payload.\";\n const signalType = payload.signalType;\n if (![\"OFFER\", \"ANSWER\", \"ICE\"].includes(signalType)) return \"Invalid signal type.\";\n if (typeof payload.to !== \"string\" || payload.to.length < 3) return \"Invalid signal destination.\";\n if (signalType !== \"ICE\" && JSON.stringify(payload.sdp || {}).length > 16 * 1024) return \"SDP too large.\";\n if (signalType === \"ICE\" && JSON.stringify(payload.candidate || {}).length > 6 * 1024) return \"ICE too large.\";\n return null;\n}\n\nfunction sanitizeRawMessage(raw) {\n if (!raw) return null;\n const text = raw.toString();\n if (Buffer.byteLength(text, \"utf8\") > MAX_MESSAGE_BYTES) return null;\n return text;\n}\n\nwss.on(\"connection\", async (socket, req) => {\n const origin = req?.headers?.origin || \"\";\n if (ALLOWED_ORIGINS.length && !ALLOWED_ORIGINS.includes(origin)) {\n socket.close(1008, \"Origin not allowed\");\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const path = req?.url || \"\";\n const query = new URL(`ws://localhost${path}`).searchParams;\n const token = query.get(\"token\");\n if (!token || token !== REQUIRED_GATEWAY_TOKEN) {\n socket.close(1008, \"Invalid gateway token\");\n return;\n }\n }\n if (REQUIRE_AUTH) {\n const result = await verifyClientToken(req);\n if (!result.ok) {\n const isLocal = origin.includes(\"localhost\") || origin.includes(\"127.0.0.1\") || !origin;\n if (!(ALLOW_DEV_ANON && isLocal)) {\n socket.close(1008, result.reason);\n return;\n }\n socket.auth = { ok: true, kind: \"dev-anon\", user: { uid: \"dev-anon\" } };\n } else {\n socket.auth = result;\n }\n }\n\n socket.clientId = `c-${Math.random().toString(36).slice(2, 8)}`;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.HELLO, { message: \"Castle Gateway online.\", clientId: socket.clientId })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.WORLD_SNAPSHOT, orchestrator.worldSnapshot())));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.BROADCAST_STATE, { broadcasterClientId, viewers: [...wss.clients].filter((c) => c.readyState === 1).length })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload())));\n broadcastExcept(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }), socket);\n\n socket.on(\"message\", (raw) => {\n const safeRaw = sanitizeRawMessage(raw);\n if (!safeRaw) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Message too large or invalid.\" })));\n return;\n }\n const parsed = safeJsonParse(safeRaw);\n if (!parsed?.type) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid message.\" })));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND_TEXT) {\n const canonical = parseTextToCommand(parsed.payload?.text);\n if (!canonical) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Unknown command text.\" })));\n return;\n }\n const result = orchestrator.applyCommand(canonical);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND) {\n const result = orchestrator.applyCommand(parsed.payload);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n }\n\n if (parsed.type === WS_MESSAGE.INPUT_FRAME) {\n const err = validateInputFrame(parsed.payload, socket);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const frame = parsed.payload.frame;\n const commands = Array.isArray(parsed.payload?.commands) ? parsed.payload.commands : [];\n orchestrator.queueInputFrame(socket.clientId, frame, commands);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, queued: true, frame })));\n }\n\n if (parsed.type === WS_MESSAGE.SIGNAL) {\n const payload = parsed.payload || {};\n const err = validateSignalPayload(payload);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const signalType = payload.signalType;\n const to = payload.to;\n for (const client of wss.clients) {\n if (client.clientId === to && client.readyState === 1) {\n client.send(JSON.stringify(createEnvelope(WS_MESSAGE.SIGNAL, { ...payload, from: socket.clientId })));\n break;\n }\n }\n }\n\n if (parsed.type === WS_MESSAGE.DRONE_TELEMETRY) {\n const payload = parsed.payload || {};\n const json = JSON.stringify(payload);\n if (Buffer.byteLength(json, \"utf8\") > 48 * 1024) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"DRONE_TELEMETRY payload too large.\" })));\n return;\n }\n broadcastExcept(createEnvelope(WS_MESSAGE.DRONE_TELEMETRY, { ...payload, relayFrom: socket.clientId }), socket);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_SOCIAL_PULSE) {\n handleCastleSocialPulse(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_WAL_PEER_FEED) {\n handleCastleWalPeerFeed(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.OPEN_DATA_QUERY) {\n queryOpenData(parsed.payload)\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: true, result })));\n })\n .catch((e) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: false, error: e.message })));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_QUERY) {\n runRhizohBrainV2({\n ...(parsed.payload || {}),\n uid: socket?.auth?.user?.uid || \"unknown\"\n })\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, result)));\n })\n .catch(async () => {\n const fallback = await runRhizohBrain(parsed.payload || {}).catch(() => ({\n ok: false,\n reply: \"Rhizoh temporary disturbance. Habitat remains stable.\"\n }));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, fallback)));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.BROADCAST_REGISTER) {\n const role = String(parsed.payload?.role || \"\");\n if (role === \"GENESIS_BROADCAST_AGENT\") {\n broadcasterClientId = socket.clientId;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, registered: role })));\n broadcastState();\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.STUDIO_CUE) {\n // Only registered broadcaster can push global cues.\n if (!broadcasterClientId || socket.clientId !== broadcasterClientId) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Only broadcaster agent can emit STUDIO_CUE.\" })));\n return;\n }\n const cue = parsed.payload || {};\n if (typeof cue !== \"object\") {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid STUDIO_CUE payload.\" })));\n return;\n }\n broadcast(createEnvelope(WS_MESSAGE.STUDIO_CUE, cue));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_JOIN_ROOM) {\n const requested = String(parsed.payload?.roomId || \"\").trim();\n const requestedName = String(parsed.payload?.roomName || \"\").trim();\n const roomId = ensureRoom(requested || `room-${Math.random().toString(36).slice(2, 7)}`, requestedName || null);\n spiralState.activeByClient.set(socket.clientId, roomId);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, roomId })));\n broadcastSpiralState();\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_CREATE_CHARACTER) {\n const name = String(parsed.payload?.name || \"\").trim().slice(0, 32) || `AI-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;\n const role = String(parsed.payload?.role || \"agent\").trim().slice(0, 24) || \"agent\";\n const roomId = ensureRoom(\n String(parsed.payload?.roomId || spiralState.activeByClient.get(socket.clientId) || spiralState.rooms[0].id),\n null\n );\n\n // Character creation always maps to an authoritative world agent.\n const result = orchestrator.applyCommand({\n command: COMMAND.SPAWN_AGENT,\n params: { targetCastleId: \"castle-01\" }\n });\n if (!result?.ok || !result?.data?.id) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Character creation failed.\" })));\n return;\n }\n const id = result.data.id;\n spiralState.characters.set(id, { id, name, role, roomId, source: \"spawned\" });\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, characterId: id, roomId })));\n broadcastSpiralState();\n return;\n }\n });\n\n socket.on(\"close\", () => {\n if (socket.clientId === broadcasterClientId) broadcasterClientId = null;\n clientStats.delete(socket.clientId);\n spiralState.activeByClient.delete(socket.clientId);\n removeCastleSocialClientFromAllRooms(socket.clientId);\n broadcast(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }));\n broadcastState();\n });\n});\n\nsetInterval(() => {\n void (async () => {\n try {\n const { frame, snapshot, delta, packedDelta } = orchestrator.tick(1 / 20);\n syncSpiralCharactersWithWorld(snapshot);\n broadcast(createEnvelope(WS_MESSAGE.NETWORK_TICK, { frame }));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_TICK, snapshot));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA, delta));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA_PACKED, packedDelta));\n broadcastSpiralState();\n await runAcademyEventTick();\n const executed = await tickAutomations(Date.now());\n for (const a of executed) {\n await addTranscript(a.uid, {\n source: \"planner\",\n eventType: \"automation-executed\",\n text: `Automation executed: ${a.title} (${a.action})`,\n roomId: \"studio-main\",\n meta: { automationId: a.id, action: a.action, deviceId: a.deviceId || \"\" }\n });\n }\n } catch (e) {\n console.error(\"[GATEWAY] world tick / automations loop error:\", String(e?.message || e));\n }\n })();\n}, 50);\n\n(async () => {\n await rhizohRuntime.telemetry.initOpenTelemetry();\n if (String(process.env.CASTLE_GENESIS_DISK_PERSIST ?? \"\").trim() === \"\") {\n process.env.CASTLE_GENESIS_DISK_PERSIST = \"1\";\n }\n const genesisDiskBoot = await hydrateGenesisContinuityPersistenceBootV0();\n if (!genesisDiskBoot.ok) {\n console.warn(\"[GATEWAY] genesis disk hydrate failed; continuing with in-memory genesis continuity\");\n }\n httpServer.listen(PORT, () => {\n startGenesisCanonicalClock();\n installGenesisCheckpointSurfaceGetter(buildGenesisRuntimeSurfacePayloadLive);\n setGenesisContinuityAfterPublishHook((seq) => {\n noteGenesisCheckpointSeqCommitted(seq);\n noteGenesisContinuitySeqAcceptedV0(seq);\n });\n startGenesisContinuityInfraSampler(buildGenesisRuntimeSurfacePayloadLive, 2000);\n logProductionObservatorySurfaceGuardsV0();\n logGatewaySubstrateAuthorityGuardsV0();\n console.log(`[GATEWAY] ws/http://localhost:${PORT}`);\n console.log(\n `[GATEWAY] genesis continuity: GET ${rhizohRuntime.routes.genesisRuntime} | POST ${rhizohRuntime.routes.genesisIngress} | SSE ${rhizohRuntime.routes.genesisStream} | replay ${rhizohRuntime.routes.genesisReplay} | diff ${rhizohRuntime.routes.genesisReplayDiff} | equiv ${rhizohRuntime.routes.genesisReplayEquivalence} | analytics ${rhizohRuntime.routes.genesisReplayAnalytics} | evolution ${rhizohRuntime.routes.genesisReplayEvolution}`\n );\n if (String(process.env.CASTLE_GENESIS_OBSERVATORY_LIVE_V01 ?? \"\").trim() === \"1\") {\n console.log(\"GENESIS OBSERVATORY LIVE v0.1 — READ ONLY PHASE STARTED\");\n }\n });\n})().catch((e) => {\n console.error(\"[GATEWAY] fatal boot (before or during listen):\", e);\n process.exit(1);\n});\n"}, "24d239d81c7129f1cb46cd4d152dee438cde803f": {"t": 1785754165.920805, "n": 153287, "h": "f26887c1b44b0366", "c": "import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport dotenv from \"dotenv\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconsole.log(\"🔥 [GENESIS_BOOT] entry file:\", __filename);\nconsole.log(\"🔥 [GENESIS_BOOT] node env:\", process.env.NODE_ENV);\nconsole.log(\"🔥 [GENESIS_BOOT] pid:\", process.pid);\nconsole.log(\"🧬 [GENESIS_BOOT] cwd:\", process.cwd());\nconsole.log(\"🧬 [GENESIS_BOOT] import.meta.url:\", import.meta.url);\ndotenv.config({ path: path.join(__dirname, \"..\", \".env\") });\ndotenv.config({ path: path.join(__dirname, \"..\", \".env.local\"), override: true });\n\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport { createServer } from \"node:http\";\nimport { createOrchestrator } from \"@castle/orchestrator\";\nimport { parseTextToCommand } from \"@castle/command-dsl\";\nimport { WS_MESSAGE, COMMAND, createEnvelope, safeJsonParse } from \"@castle/protocol\";\nimport { queryOpenData } from \"./openData.js\";\nimport { verifyClientToken } from \"./auth.js\";\nimport { runRhizohBrain } from \"./rhizohBrain.js\";\nimport { queryRhizohLlm } from \"./rhizohLlmGateway.js\";\nimport { rhizohGatewayTurn } from \"./rhizohGatewayTurn.js\";\nimport {\n RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n runRhizohVoiceTranscribeV3,\n rhizohVoiceTranscribeEnvV3\n} from \"./rhizohVoiceTranscribeV3.js\";\nimport {\n meshAppendDelta,\n meshContinuityAggregate,\n meshJoin,\n meshLeave,\n meshReplayFromNodeId,\n meshReplayFromSeq,\n meshSnapshot,\n meshSubscribeSse\n} from \"./presenceRoomMesh.js\";\nimport { runRhizohBrainV2 } from \"./rhizohBrainV2.js\";\nimport {\n listConnections,\n createConnection,\n updateConnection,\n deleteConnection,\n setDefaultConnection,\n resolveConnection\n} from \"./llmConnectionsStore.js\";\nimport { checkHttpRateLimit, getHttpClientIp } from \"./castleHttpRateLimit.js\";\nimport {\n submitAbuseReportV0,\n listModerationQueueV0,\n softBlockUserV0,\n removeSoftBlockV0,\n listSoftBlockedUsersV0\n} from \"./ops/moderationMvpV0.js\";\nimport { listRecentAgentSnapshotsV0 } from \"./ops/agentObservabilityV0.js\";\nimport { readAgentContainmentConfigV0 } from \"./ops/agentContainmentV0.js\";\nimport { readCostContainmentConfigV0, getCostContainmentStatsV0 } from \"./ops/costContainmentV0.js\";\nimport { getGlobalCostLedgerSnapshotV0, getGclBackendStatusV0 } from \"./ops/globalCostLedgerV0.js\";\nimport { evaluateEconomicStrategyL3V0 } from \"./ops/economicStrategyEngineL3V0.js\";\nimport { getPhasedRolloutStatsV0, readPhasedRolloutTierV0 } from \"./ops/phasedRolloutV0.js\";\nimport { buildUnifiedStateNarrativeV0 } from \"./ops/unifiedStateNarrativeV0.js\";\nimport { resolveHardeningTenantIdV0 } from \"./ops/narrativeTenantIsolationV0.js\";\nimport {\n classifyStressResponseV0,\n verifyStressTaxonomyCoverageV0,\n verifyStressConflictResolutionV0\n} from \"./ops/stressResponseTaxonomyV0.js\";\nimport { verifyResolutionStabilityEnvelopeV0 } from \"./ops/resolutionStabilityEnvelopeV0.js\";\nimport { logLlmAccess } from \"./castleLlmAudit.js\";\nimport { appendMemory, listMemories, getMemoryContext, getPersonaGoalMemory, setPersonaGoalMemory, autoCompactMemories } from \"./memoryStore.js\";\nimport { getFirebasePersistence } from \"./firebasePersistence.js\";\nimport { registerAgentIdentity, listAgentIdentities, getAgentIdentity, updateAgentIdentity } from \"./agentIdentityStore.js\";\nimport { queueAcademyEvent, listAcademyEvents, resolveAcademyEvent, runAcademyEventTick } from \"./academyEventEngine.js\";\nimport { enqueuePdfJob, getPdfJob } from \"./pdfPipeline.js\";\nimport {\n createPublishSession,\n listPublishSessions,\n updatePublishSession,\n addTranscript,\n listTranscripts,\n searchMetadata,\n registerDevice,\n listDevices,\n updateDevice,\n appendTelemetry,\n listTelemetry,\n createAutomation,\n listAutomations,\n tickAutomations,\n enqueueCommand,\n listCommands,\n updateCommandStatus,\n registerSocialChannel,\n listSocialChannels\n} from \"./studioOpsStore.js\";\nimport { metrics as infraMetrics } from \"./infra/metrics.js\";\nimport { renderRhizohPrometheusMetrics, rhizohEnterpriseMetrics } from \"./infra/rhizohEnterpriseMetrics.js\";\nimport { scoreHealth } from \"./infra/healthScore.js\";\nimport { getTraceById, listTracesBySession } from \"./infra/traceRegistry.js\";\nimport { canonicalEpistemicSealString, hashAndSignEpistemicSeal, EPISTEMIC_SEAL_SCHEMA } from \"./epistemicSeal.js\";\nimport { persistEpistemicLedgerBatch } from \"./epistemicLedgerStore.js\";\nimport { persistEpistemicForecastBatch } from \"./epistemicForecastStore.js\";\nimport { buildRhizohExternalGroundTruthPayload } from \"./rhizohExternalGroundTruthGateway.js\";\nimport { ingestRhizohExternalLossBatchHttp } from \"./rhizohExternalLossIngestGateway.js\";\nimport {\n ingestRhizohProductOutcomeHttp,\n listRhizohProductOutcomeAggregates,\n verifyRhizohOutcomeHmac,\n verifyRhizohOutcomeSourceToken\n} from \"./rhizohProductOutcomeIngestGateway.js\";\nimport { initRhizoh } from \"./rhizohProductionBootstrap.js\";\nimport { handleAcademicObservatoryExportGetV0 } from \"./rhizoh/academicObservatoryHttpV0.js\";\nimport {\n buildGenesisRuntimeSurfacePayload,\n recordGenesisEpistemicLedgerPersisted,\n recordGenesisEpistemicSealIssued,\n startGenesisCanonicalClock\n} from \"./genesisRuntimeSurfaceV0.js\";\nimport { registerGenesisSseClient, getGenesisContinuitySeq, setGenesisContinuityAfterPublishHook } from \"./genesisContinuityStreamHubV0.js\";\nimport { computeGenesisReplayFingerprintV0 } from \"./genesisReplayFingerprintV0.js\";\nimport { buildGenesisGatewayCapabilitiesV0 } from \"./genesisGatewayCapabilitiesV0.js\";\nimport {\n installGenesisCheckpointSurfaceGetter,\n noteGenesisCheckpointSeqCommitted,\n getLatestGenesisSignedCheckpoint\n} from \"./genesisContinuityCheckpointV0.js\";\nimport { hydrateGenesisContinuityPersistenceBootV0 } from \"./genesisContinuityHydrateBootV0.js\";\nimport { startGenesisContinuityInfraSampler } from \"./genesisContinuityInfraSamplerV0.js\";\nimport {\n genesisCheckpointQueryBySeqV0,\n genesisCheckpointQueryRangeV0,\n genesisCheckpointQueryLineageV0\n} from \"./genesisCheckpointQueryV0.js\";\nimport {\n genesisContinuityEventArchiveEnabled,\n queryGenesisContinuityEventArchiveV0,\n GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION\n} from \"./genesisContinuityEventArchiveV0.js\";\nimport { resolveGenesisReplayRouterV1, GENESIS_REPLAY_ROUTER_SCHEMA } from \"./genesisReplayRouterV1.js\";\nimport {\n computeGenesisReplayTemporalDiffV1,\n GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA\n} from \"./genesisReplayTemporalDiffV1.js\";\nimport { compareGenesisReplayEquivalenceV1 } from \"./genesisReplayEquivalenceV1.js\";\nimport { computeGenesisReplayAnalyticsV1 } from \"./genesisReplayAnalyticsV1.js\";\nimport { computeGenesisReplayEvolutionV1 } from \"./genesisReplayEvolutionV1.js\";\nimport { handleStripeCheckoutCreate, handleStripeMembershipWebhook } from \"./stripeWebhookMembership.js\";\nimport { buildRealityHealthPayload } from \"./edgeRealityConsistencyV1.js\";\nimport {\n assertWalPeerSocketAuthorityV0,\n buildGatewaySubstrateAuthoritySnapshotV0,\n logGatewaySubstrateAuthorityGuardsV0,\n validateWalPeerFeedSignatureV0\n} from \"./gatewaySubstrateAuthorityV0.js\";\nimport {\n buildSubstrateOperationalSnapshotV0,\n recordWalPeerRejectV0,\n renderSubstratePrometheusMetricsV0\n} from \"./infra/substrateOperationalMetrics.js\";\nimport { ingestClientSubstrateHealthV0 } from \"./rhizohSubstrateHealthIngestV0.js\";\nimport { recordSubstrateOtelEventV0 } from \"./infra/opentelemetrySubstrateV0.js\";\nimport {\n GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n getGenesisWorldObservationIngressStatusV0,\n ingestGenesisWorldObservationV0\n} from \"./genesisContinuityClientIngressV0.js\";\nimport {\n getGenesisContinuitySeqAuditV0,\n noteGenesisContinuitySeqAcceptedV0\n} from \"./genesisContinuitySeqGapV0.js\";\n\n/** Render / Fly / Railway: `PORT` — yoksa `CASTLE_GATEWAY_PORT` — yoksa 8090. */\nconst PORT =\n Number(process.env.PORT) ||\n Number(process.env.CASTLE_GATEWAY_PORT) ||\n 8090;\n/** Spiral / world WebSocket server — set after `wss` init; used by genesis runtime surface. */\nlet spiralWssForGenesis = null;\nconst MAX_MESSAGE_BYTES = Number(process.env.CASTLE_MAX_MESSAGE_BYTES || 32 * 1024);\nconst REQUIRED_GATEWAY_TOKEN = process.env.CASTLE_GATEWAY_TOKEN || \"\";\nconst ALLOWED_ORIGINS = (process.env.CASTLE_ALLOWED_ORIGINS || \"\")\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n\nfunction normalizeHttpOrigin(origin) {\n return String(origin || \"\")\n .trim()\n .replace(/\\/+$/, \"\");\n}\n\n/** HTTP CORS: `CASTLE_ALLOWED_ORIGINS` + `CASTLE_HTTP_CORS_ORIGIN` (tek origin); `CASTLE_HTTP_CORS_ORIGIN=*` → wildcard. */\nfunction buildHttpCorsOriginAllowSet() {\n const set = new Set();\n for (const o of ALLOWED_ORIGINS) {\n const n = normalizeHttpOrigin(o);\n if (n) set.add(n);\n }\n const primary = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n if (primary && primary !== \"*\") set.add(primary);\n return set;\n}\n\nconst HTTP_CORS_ORIGIN_ALLOW_SET = buildHttpCorsOriginAllowSet();\n\n/**\n * Tarayıcı `Origin` ile tam eşleşme ister. Tek sabit `CASTLE_HTTP_CORS_ORIGIN` yeterli değil:\n * örn. `web.app` vs `firebaseapp.com` veya Render’da yanlış/eksik env.\n * @returns {string|null} `Access-Control-Allow-Origin` değeri; null = başlık yok (istemci CORS fail).\n */\nfunction accessControlAllowOriginValue(req) {\n const primaryEnv = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n if (primaryEnv === \"*\") return \"*\";\n const reqOrigin = normalizeHttpOrigin(req.headers?.origin);\n if (HTTP_CORS_ORIGIN_ALLOW_SET.size === 0) return \"*\";\n if (!reqOrigin) return \"*\";\n if (HTTP_CORS_ORIGIN_ALLOW_SET.has(reqOrigin)) return reqOrigin;\n return null;\n}\n\nfunction applyHttpCorsHeaders(req, res) {\n const allow = accessControlAllowOriginValue(req);\n if (allow) {\n res.setHeader(\"Access-Control-Allow-Origin\", allow);\n if (allow !== \"*\") res.setHeader(\"Vary\", \"Origin\");\n }\n res.setHeader(\"Access-Control-Allow-Methods\", \"GET,POST,OPTIONS\");\n res.setHeader(\n \"Access-Control-Allow-Headers\",\n \"Content-Type, Authorization, X-Castle-Dev-Uid, X-Castle-Guest-Id, X-Castle-Gateway-Token, X-Castle-Ingress-Contract, X-Rhizoh-Outcome-Signature, X-Rhizoh-Outcome-Source-Token, X-Castle-Academic-Observatory-Key, X-Castle-Moderation-Key\"\n );\n}\nconst REQUIRE_AUTH = process.env.CASTLE_REQUIRE_AUTH === \"true\";\nconst ALLOW_DEV_ANON = process.env.CASTLE_ALLOW_DEV_ANON !== \"false\";\nconst ALLOW_DEV_HTTP_UID = process.env.CASTLE_ALLOW_DEV_HTTP_UID !== \"false\";\n/** Presence mesh: allow `X-Castle-Guest-Id` when Firebase auth absent (default on). Set `false` to require auth only. */\nconst ALLOW_MESH_GUEST = process.env.CASTLE_ALLOW_MESH_GUEST !== \"false\";\nconst SOCIAL_RETRY_MAX = Math.max(0, Math.min(5, Number(process.env.CASTLE_SOCIAL_RETRY_MAX || 2)));\nconst SOCIAL_RETRY_BASE_MS = Math.max(100, Number(process.env.CASTLE_SOCIAL_RETRY_BASE_MS || 700));\nconst TELEGRAM_BOT_TOKEN = process.env.CASTLE_TELEGRAM_BOT_TOKEN || \"\";\nconst WHATSAPP_TOKEN = process.env.CASTLE_WHATSAPP_TOKEN || \"\";\nconst WHATSAPP_PHONE_NUMBER_ID = process.env.CASTLE_WHATSAPP_PHONE_NUMBER_ID || \"\";\nconst RL_RHIZOH_LLM_PER_MIN = Math.max(5, Number(process.env.CASTLE_RL_RHIZOH_LLM_PER_MIN || 40));\nconst RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN || 30)\n);\nconst RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN || 120));\nconst RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN = Math.max(8, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN || 48));\nconst RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN = Math.max(4, Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN || 24));\nconst RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN || 18)\n);\nconst RL_EPISTEMIC_SEAL_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_EPISTEMIC_SEAL_PER_MIN || 120));\nconst RL_EPISTEMIC_LOG_PER_MIN = Math.max(20, Number(process.env.CASTLE_RL_EPISTEMIC_LOG_PER_MIN || 240));\nconst EPISTEMIC_SEAL_SECRET = String(\n process.env.CASTLE_EPISTEMIC_SEAL_SECRET || process.env.CASTLE_GATEWAY_TOKEN || \"\"\n).trim();\nconst RL_LLM_CONN_TEST_PER_MIN = Math.max(2, Number(process.env.CASTLE_RL_LLM_CONNECTION_TEST_PER_MIN || 8));\n\n/** One-shot production warnings: CORS surface, auth toggle, dev bypass flags (observatory deploy hygiene). */\nfunction logProductionObservatorySurfaceGuardsV0() {\n if (String(process.env.NODE_ENV || \"\").toLowerCase() !== \"production\") return;\n const tag = \"[GATEWAY_PROD_GUARD]\";\n const primaryCors = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n const corsListEmpty = ALLOWED_ORIGINS.length === 0;\n if (corsListEmpty && !primaryCors) {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty and CASTLE_HTTP_CORS_ORIGIN empty — browser SSE/replay may fail; set explicit Firebase Hosting origins`\n );\n } else if (corsListEmpty && primaryCors && primaryCors !== \"*\") {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty — only CASTLE_HTTP_CORS_ORIGIN set; add web.app + firebaseapp.com (and previews) to CASTLE_ALLOWED_ORIGINS if clients use alternate hostnames`\n );\n }\n if (primaryCors && /localhost|127\\.0\\.0\\.1/i.test(primaryCors)) {\n console.warn(`${tag} CORS: CASTLE_HTTP_CORS_ORIGIN is localhost while NODE_ENV=production`);\n }\n if (REQUIRE_AUTH) {\n console.warn(\n `${tag} AUTH: CASTLE_REQUIRE_AUTH=true — clients must send a valid gateway credential (otherwise silent 401 on replay/SSE); EventSource cannot set custom headers unless proxied`\n );\n if (REQUIRED_GATEWAY_TOKEN.length < 16) {\n console.warn(`${tag} AUTH: CASTLE_GATEWAY_TOKEN missing or short while require-auth enabled`);\n }\n }\n if (ALLOW_DEV_ANON) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_ANON is not \"false\" — dev anonymous path may be live in production (set CASTLE_ALLOW_DEV_ANON=false)`\n );\n }\n if (ALLOW_DEV_HTTP_UID) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_HTTP_UID is not \"false\" — X-Castle-Dev-Uid bypass risk (set CASTLE_ALLOW_DEV_HTTP_UID=false)`\n );\n }\n}\n\nconst rhizohRuntime = initRhizoh();\n\n/** İstemci: llmKeySource veya keySource — env | user_connection | auto */\nfunction normalizeClientLlmKeySource(raw) {\n const s = String(raw ?? \"\").trim().toLowerCase();\n if (s === \"env\" || s === \"server\") return \"env\";\n if (s === \"user_connection\" || s === \"connection\" || s === \"user\") return \"user_connection\";\n if (s === \"auto\" || s === \"\") return \"auto\";\n return null;\n}\n\nfunction sendJson(res, statusCode, payload) {\n res.writeHead(statusCode, { \"Content-Type\": \"application/json; charset=utf-8\" });\n res.end(JSON.stringify(payload));\n}\n\nfunction logRhizohHealth(stage, detail = {}) {\n try {\n console.info(`[CASTLE_${String(stage || \"unknown\")}]`, detail && typeof detail === \"object\" ? detail : {});\n } catch {\n /* noop */\n }\n}\n\nfunction renderInfraMetricsProm() {\n const lines = [\n `castle_gateway_events_processed_total ${infraMetrics.eventsProcessed}`,\n `castle_gateway_duplicate_reject_total ${infraMetrics.duplicateRejects}`,\n `castle_gateway_enqueue_latency_ms ${infraMetrics.enqueueLatencyMs}`,\n `castle_gateway_queue_depth ${infraMetrics.queueDepth}`,\n `castle_gateway_queue_lag_seconds ${(Number(infraMetrics.queueLag || 0) / 1000).toFixed(3)}`,\n `castle_gateway_errors_total ${infraMetrics.errors}`\n ];\n const buckets = infraMetrics.enqueueLatencyBuckets || {};\n for (const k of Object.keys(buckets)) {\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"${k}\"} ${buckets[k]}`);\n }\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"+Inf\"} ${infraMetrics.eventsProcessed}`);\n if (process.env.CASTLE_SUBSTRATE_METRICS !== \"0\") {\n lines.push(renderSubstratePrometheusMetricsV0().trimEnd());\n }\n return lines.join(\"\\n\");\n}\n\nasync function fetchJsonWithTimeout(url, timeoutMs = 1200) {\n const ctl = new AbortController();\n const tid = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const res = await fetch(url, { signal: ctl.signal });\n if (!res.ok) return null;\n return await res.json();\n } catch {\n return null;\n } finally {\n clearTimeout(tid);\n }\n}\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction getHttpPathname(req) {\n let p = String(req.url || \"\").split(\"?\")[0];\n /* Reverse proxies / clients sometimes append `/` — avoid silent 404 on e.g. `/rhizoh/genesis/stream/`. */\n while (p.length > 1 && p.endsWith(\"/\")) {\n p = p.slice(0, -1);\n }\n return p;\n}\n\nfunction rhizohLlmEnvConfigured() {\n const p = String(process.env.CASTLE_LLM_PROVIDER || \"openai\").toLowerCase();\n const key =\n p === \"anthropic\"\n ? process.env.ANTHROPIC_API_KEY\n : p === \"gemini\"\n ? process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY\n : p === \"xai\"\n ? process.env.XAI_API_KEY\n : p === \"deepseek\"\n ? process.env.DEEPSEEK_API_KEY\n : p === \"mistral\"\n ? process.env.MISTRAL_API_KEY\n : p === \"openrouter\"\n ? process.env.OPENROUTER_API_KEY\n : process.env.OPENAI_API_KEY;\n return !!String(key || \"\").trim();\n}\n\n/**\n * @param {import(\"firebase-admin/firestore\").Firestore | null} db\n * @param {number} timeoutMs\n */\nasync function probeFirestoreReachable(db, timeoutMs = 2000) {\n if (!db) return { ok: false, reason: \"no_db\" };\n const timeout = sleep(timeoutMs).then(() => ({ ok: false, reason: \"timeout\" }));\n const work = db\n .collection(\"_castle_gateway_health\")\n .doc(\"ping\")\n .get()\n .then(() => ({ ok: true, reason: \"reachable\" }))\n .catch((e) => ({ ok: false, reason: String(e?.message || e || \"firestore_error\") }));\n return Promise.race([work, timeout]);\n}\n\nfunction stripHtml(text = \"\") {\n return String(text)\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \" \")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \" \")\n .replace(/<[^>]+>/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction haversineMeters(aLat, aLon, bLat, bLon) {\n const R = 6371000;\n const dLat = ((bLat - aLat) * Math.PI) / 180;\n const dLon = ((bLon - aLon) * Math.PI) / 180;\n const sa = Math.sin(dLat / 2);\n const sb = Math.sin(dLon / 2);\n const x = sa * sa + Math.cos((aLat * Math.PI) / 180) * Math.cos((bLat * Math.PI) / 180) * sb * sb;\n return 2 * R * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));\n}\n\nasync function checkPublishTargetHealth(protocol, target) {\n const p = String(protocol || \"\").toUpperCase();\n const t = String(target || \"\").trim();\n if (!t) return { ok: false, reason: \"target_missing\", shouldReconnect: false };\n if (p === \"RTMP\") {\n const ok = t.startsWith(\"rtmp://\") || t.startsWith(\"rtmps://\");\n return { ok, reason: ok ? \"rtmp_target_format_ok\" : \"invalid_rtmp_target\", shouldReconnect: !ok, reconnectInSec: 15 };\n }\n if (p === \"WHIP\" || p === \"SFU\") {\n try {\n const res = await fetch(t, { method: \"OPTIONS\" });\n const ok = res.status < 500;\n return { ok, reason: `http_${res.status}`, shouldReconnect: !ok, reconnectInSec: 8 };\n } catch {\n return { ok: false, reason: \"network_unreachable\", shouldReconnect: true, reconnectInSec: 8 };\n }\n }\n return { ok: true, reason: \"unchecked_protocol\", shouldReconnect: false };\n}\n\nasync function fetchTextFromUrl(url) {\n const res = await fetch(url, { method: \"GET\" });\n if (!res.ok) throw new Error(`fetch_${res.status}`);\n const ct = String(res.headers.get(\"content-type\") || \"\");\n if (ct.includes(\"application/pdf\")) throw new Error(\"pdf_binary_not_supported_directly\");\n const body = await res.text();\n return stripHtml(body).slice(0, 14000);\n}\n\nfunction buildTaskActionGraph(task = \"\", context = {}) {\n const t = String(task || \"\").toLowerCase();\n if (t.includes(\"dinner\") || t.includes(\"akşam yemeği\") || t.includes(\"aksam yemegi\")) {\n return {\n taskTitle: \"Dinner Prep Planner\",\n nodes: [\n { id: \"n1\", title: \"Kitchen preheat\", action: \"smart-oven.preheat\", params: { temperatureC: 190, mode: \"fan\" } },\n { id: \"n2\", title: \"Lighting comfort\", action: \"smart-light.set_scene\", params: { scene: \"warm_dinner\" } },\n { id: \"n3\", title: \"Fridge inventory check\", action: \"smart-fridge.inventory_scan\", params: {} },\n { id: \"n4\", title: \"Reminder notification\", action: \"smart-watch.notify\", params: { text: \"Dinner prep started.\" } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" },\n { from: \"n3\", to: \"n4\" }\n ]\n };\n }\n return {\n taskTitle: task || \"General Planner\",\n nodes: [\n { id: \"n1\", title: \"Safety check\", action: \"device.safety_check\", params: { context } },\n { id: \"n2\", title: \"Primary action\", action: \"device.execute_task\", params: { task } },\n { id: \"n3\", title: \"Report\", action: \"studio.timeline_note\", params: { task } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" }\n ]\n };\n}\n\nfunction buildEthicsPrograms() {\n return {\n policyVersion: \"v1\",\n tracks: [\n {\n id: \"elderly-care-assist\",\n audience: \"elderly\",\n title: \"Elderly Assistance Track\",\n principles: [\"human-in-the-loop\", \"consent-first\", \"low-friction alerts\"],\n modules: [\"medication-reminder\", \"fall-risk-check\", \"family-notify\", \"daily-companion-brief\"]\n },\n {\n id: \"kids-safe-learning\",\n audience: \"children\",\n title: \"Kids Safe Learning Track\",\n principles: [\"guardian-consent\", \"age-appropriate-language\", \"content-filtering\"],\n modules: [\"study-coach\", \"screen-time-balance\", \"safe-social-share\", \"emergency-contact\"]\n }\n ]\n };\n}\n\nasync function deliverViaTelegram(channel, text) {\n const chatId = String(channel.endpoint || \"\").trim();\n if (!TELEGRAM_BOT_TOKEN) throw new Error(\"telegram_token_missing\");\n if (!chatId) throw new Error(\"telegram_chat_id_missing\");\n const url = `https://api.telegram.org/bot${encodeURIComponent(TELEGRAM_BOT_TOKEN)}/sendMessage`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ chat_id: chatId, text })\n });\n if (!res.ok) throw new Error(`telegram_http_${res.status}`);\n return { provider: \"telegram\", status: res.status };\n}\n\nasync function deliverViaWhatsapp(channel, text) {\n const to = String(channel.endpoint || \"\").trim();\n if (!WHATSAPP_TOKEN) throw new Error(\"whatsapp_token_missing\");\n if (!WHATSAPP_PHONE_NUMBER_ID) throw new Error(\"whatsapp_phone_number_id_missing\");\n if (!to) throw new Error(\"whatsapp_to_missing\");\n const url = `https://graph.facebook.com/v22.0/${encodeURIComponent(WHATSAPP_PHONE_NUMBER_ID)}/messages`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${WHATSAPP_TOKEN}` },\n body: JSON.stringify({ messaging_product: \"whatsapp\", to, type: \"text\", text: { body: text } })\n });\n if (!res.ok) throw new Error(`whatsapp_http_${res.status}`);\n return { provider: \"whatsapp\", status: res.status };\n}\n\nasync function deliverViaWebhook(channel, text) {\n const endpoint = String(channel.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"webhook_endpoint_missing\");\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text, platform: channel.platform || \"webhook\", ts: Date.now() })\n });\n if (!res.ok) throw new Error(`webhook_http_${res.status}`);\n return { provider: \"webhook\", status: res.status };\n}\n\nasync function deliverSocialMessage(channel, text) {\n const platform = String(channel.platform || \"webhook\").toLowerCase();\n if (platform === \"telegram\") return deliverViaTelegram(channel, text);\n if (platform === \"whatsapp\") return deliverViaWhatsapp(channel, text);\n return deliverViaWebhook(channel, text);\n}\n\nasync function deliverSocialWithRetry(channel, text) {\n let attempt = 0;\n let lastError = null;\n while (attempt <= SOCIAL_RETRY_MAX) {\n try {\n const result = await deliverSocialMessage(channel, text);\n return { ok: true, attempt: attempt + 1, ...result };\n } catch (error) {\n lastError = error;\n if (attempt >= SOCIAL_RETRY_MAX) break;\n const jitter = Math.floor(Math.random() * 90);\n await sleep(SOCIAL_RETRY_BASE_MS * Math.pow(2, attempt) + jitter);\n }\n attempt += 1;\n }\n return { ok: false, attempt: attempt + 1, error: String(lastError?.message || \"delivery_failed\") };\n}\n\nasync function dispatchCommandToAdapter(device, command) {\n const adapter = String(command?.adapter || device?.adapter || \"websocket\").toLowerCase();\n const endpoint = String(device?.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"device_endpoint_missing\");\n if (adapter === \"mqtt\") {\n // MQTT delivery is proxied via a bridge endpoint (HTTP webhook style).\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n type: \"mqtt-command\",\n topic: `castle/devices/${device.id}/commands`,\n action: command.action,\n params: command.params || {}\n })\n });\n if (!res.ok) throw new Error(`mqtt_bridge_http_${res.status}`);\n return { ok: true, adapter, via: \"http-bridge\", status: res.status };\n }\n if (!endpoint.startsWith(\"ws://\") && !endpoint.startsWith(\"wss://\")) throw new Error(\"invalid_websocket_endpoint\");\n const sendResult = await new Promise((resolve, reject) => {\n const ws = new WebSocket(endpoint);\n const timer = setTimeout(() => {\n try {\n ws.close();\n } catch {\n /* noop */\n }\n reject(new Error(\"websocket_timeout\"));\n }, 3000);\n ws.on(\"open\", () => {\n ws.send(\n JSON.stringify({\n type: \"castle-command\",\n commandId: command.id,\n deviceId: command.deviceId,\n action: command.action,\n params: command.params || {}\n }),\n (err) => {\n clearTimeout(timer);\n ws.close();\n if (err) reject(new Error(\"websocket_send_failed\"));\n else resolve({ ok: true, adapter: \"websocket\", via: \"ws\" });\n }\n );\n });\n ws.on(\"error\", () => {\n clearTimeout(timer);\n reject(new Error(\"websocket_connect_failed\"));\n });\n });\n return sendResult;\n}\n\nfunction readHttpJson(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => {\n if (!buf) return resolve({});\n const payload = safeJsonParse(buf);\n if (!payload || typeof payload !== \"object\") return reject(new Error(\"invalid_json_payload\"));\n resolve(payload);\n });\n req.on(\"error\", reject);\n });\n}\n\n/** Ham gövde (HMAC doğrulaması için). */\nfunction readHttpBodyText(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => resolve(buf));\n req.on(\"error\", reject);\n });\n}\n\nfunction maskOpaqueId(value) {\n const t = String(value || \"\");\n if (!t) return \"(empty)\";\n if (t.length <= 8) return \"****\";\n return `${t.slice(0, 4)}…${t.slice(-2)}`;\n}\n\nfunction readGatewayHttpToken(req) {\n const x = String(req.headers?.[\"x-castle-gateway-token\"] || \"\").trim();\n if (x) return x;\n const auth = String(req.headers?.authorization || \"\");\n const m = /^Bearer\\s+(.+)$/i.exec(auth);\n return m ? String(m[1] || \"\").trim() : \"\";\n}\n\nasync function resolveHttpUser(req) {\n const authResult = await verifyClientToken(req);\n if (authResult?.ok && authResult.user?.uid) {\n return { ok: true, uid: String(authResult.user.uid), mode: authResult.kind || \"auth\" };\n }\n if (ALLOW_DEV_ANON && ALLOW_DEV_HTTP_UID) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) return { ok: true, uid: `dev-${devUid.slice(0, 64)}`, mode: \"dev-header\" };\n }\n return { ok: false, reason: authResult?.reason || \"auth_required\" };\n}\n\n/** Mesh actor: Firebase uid, dev header, or `guest:<uuid>` via header / `guestId` query (SSE cannot set headers). */\nasync function resolveMeshActor(req) {\n const auth = await resolveHttpUser(req);\n if (auth.ok) return { ok: true, clientUid: auth.uid, mode: auth.mode };\n if (ALLOW_MESH_GUEST) {\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const gid = String(req.headers?.[\"x-castle-guest-id\"] || q.searchParams.get(\"guestId\") || \"\").trim();\n if (gid) {\n const normalized = gid.startsWith(\"guest:\") ? gid.slice(0, 180) : `guest:${gid.slice(0, 160)}`;\n return { ok: true, clientUid: normalized, mode: \"guest-header\" };\n }\n }\n return { ok: false, reason: \"mesh_auth_required\" };\n}\n\n/** Live genesis runtime JSON (GET /rhizoh/genesis/runtime + SSE infra sampler). */\nasync function buildGenesisRuntimeSurfacePayloadLive() {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n let spiralWsActive = 0;\n if (spiralWssForGenesis) {\n for (const c of spiralWssForGenesis.clients) {\n if (c.readyState === 1) spiralWsActive += 1;\n }\n }\n const base = buildGenesisRuntimeSurfacePayload({\n infraMetrics,\n scoredHealth: scored,\n rhizohEnterpriseMetrics,\n mesh: meshContinuityAggregate(),\n workerHealth: worker,\n port: PORT,\n spiralWebSocketClientsActive: spiralWsActive\n });\n return {\n ...base,\n replayFingerprint: computeGenesisReplayFingerprintV0(base),\n gatewayCapabilities: buildGenesisGatewayCapabilitiesV0(),\n genesisStream: {\n schema: \"castle.genesis.stream_cursor.v0\",\n lastAcceptedSeq: getGenesisContinuitySeq(),\n clientIngress: {\n schema: GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n contract: \"castle.world_observation.ingress_envelope.v1\",\n postPath: rhizohRuntime.routes.genesisIngress,\n closure: getGenesisWorldObservationIngressStatusV0()\n },\n seqContinuity: getGenesisContinuitySeqAuditV0(),\n eventArchive: genesisContinuityEventArchiveEnabled()\n ? {\n enabled: true,\n projection: GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION,\n queryPath: rhizohRuntime.routes.genesisContinuityEvents\n }\n : { enabled: false },\n replayRouter: {\n schema: GENESIS_REPLAY_ROUTER_SCHEMA,\n queryPath: rhizohRuntime.routes.genesisReplay,\n queryHint: \"from=&to= or range=from-to; optional type=&checkpoints=0\",\n temporalDiffPath: rhizohRuntime.routes.genesisReplayDiff,\n temporalDiffSchema: GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA,\n equivalencePath: rhizohRuntime.routes.genesisReplayEquivalence,\n analyticsPath: rhizohRuntime.routes.genesisReplayAnalytics,\n evolutionPath: rhizohRuntime.routes.genesisReplayEvolution\n }\n }\n };\n}\n\nconst httpServer = createServer(async (req, res) => {\n applyHttpCorsHeaders(req, res);\n if (req.method === \"OPTIONS\") {\n res.writeHead(204);\n res.end();\n return;\n }\n\n const pathname = getHttpPathname(req);\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/genesis/__ping\") {\n sendJson(res, 200, {\n ok: true,\n pid: process.pid,\n entry: __filename,\n time: Date.now(),\n genesis: true\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/webhooks/stripe\") {\n await handleStripeMembershipWebhook(req, res);\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/billing/stripe/checkout\") {\n await handleStripeCheckoutCreate(req, res, { readHttpJson, resolveHttpUser, sendJson });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/live\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, live: false, dns: true, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n sendJson(res, 200, { ok: true, live: true, dns: true, service: \"castle-gateway\", ts: Date.now() });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/ready\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, ready: false, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n try {\n const persistence = getFirebasePersistence();\n const llmProbe = rhizohLlmEnvConfigured();\n sendJson(res, 200, {\n ok: true,\n ready: true,\n dns: true,\n persistence: persistence.mode,\n service: \"castle-gateway\",\n gateway: \"ok\",\n llm: llmProbe ? \"ok\" : \"degraded\",\n spine: llmProbe ? \"ok\" : \"partial\"\n });\n } catch (e) {\n sendJson(res, 503, { ok: false, ready: false, reason: String(e?.message || e), service: \"castle-gateway\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/reality\") {\n const payload = buildRealityHealthPayload();\n sendJson(res, payload.ok ? 200 : 503, payload);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/substrate\") {\n const authority = buildGatewaySubstrateAuthoritySnapshotV0();\n const operational = buildSubstrateOperationalSnapshotV0();\n const drift = buildRealityHealthPayload();\n const ok = Boolean(authority.productionAuthoritySatisfied) && drift.ok;\n sendJson(res, ok ? 200 : 503, {\n ok,\n service: \"castle-gateway\",\n ts: Date.now(),\n authority,\n operational,\n drift: { ok: drift.ok, errors: drift.drift?.errors, warnings: drift.drift?.warnings }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/substrate/health\") {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const body = await readHttpJson(req);\n const uid = (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestClientSubstrateHealthV0(body, { uid, clientId: uid });\n if (!result.ok) {\n sendJson(res, 400, result);\n return;\n }\n recordSubstrateOtelEventV0(\"client_health_ingest\", { uid: String(uid).slice(0, 32) });\n sendJson(res, 200, result);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/deps\") {\n const t0 = Date.now();\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, {\n ok: false,\n dns: true,\n llm: false,\n firestore: false,\n phase: \"maintenance\",\n latencyMs: Date.now() - t0,\n service: \"castle-gateway\"\n });\n return;\n }\n const persistence = getFirebasePersistence();\n let firestoreOk = persistence.mode !== \"firebase\";\n let firestoreDetail = persistence.mode === \"file\" ? \"file_mode\" : \"not_applicable\";\n if (persistence.mode === \"firebase\" && persistence.db) {\n const pr = await probeFirestoreReachable(persistence.db, 2000);\n firestoreOk = pr.ok;\n firestoreDetail = pr.reason || (pr.ok ? \"reachable\" : \"fail\");\n } else if (persistence.mode === \"firebase\" && !persistence.db) {\n firestoreOk = false;\n firestoreDetail = \"firebase_not_initialized\";\n }\n const llmOk = rhizohLlmEnvConfigured();\n const overallOk = llmOk && firestoreOk;\n const latencyMs = Date.now() - t0;\n sendJson(res, 200, {\n ok: overallOk,\n dns: true,\n llm: llmOk,\n firestore: persistence.mode === \"firebase\" ? firestoreOk : false,\n persistence: persistence.mode,\n firestoreDetail,\n latencyMs,\n service: \"castle-gateway\",\n wsPort: PORT\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health\") {\n const persistence = getFirebasePersistence();\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n wsPort: PORT,\n persistence: persistence.mode,\n presenceMesh: \"sse\"\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/health\") {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n sendJson(res, 200, {\n ok: true,\n role: \"gateway\",\n queueLagMs: infraMetrics.queueLag,\n queueDepth: infraMetrics.queueDepth,\n errors: infraMetrics.errors,\n status: scored.status,\n score: scored.score,\n reasons: scored.reasons,\n worker: worker\n ? {\n status: worker.status,\n score: worker.score,\n reasons: worker.reasons\n }\n : null,\n timestamp: Date.now()\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisRuntime) {\n const payload = await buildGenesisRuntimeSurfacePayloadLive();\n sendJson(res, 200, payload);\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.genesisIngress) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const actor = await resolveMeshActor(req);\n const clientId = actor.ok ? actor.clientUid : (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestGenesisWorldObservationV0(body, { clientId });\n if (!result.ok) {\n const status =\n result.error === \"rate_limited\" || result.error === \"agent_spoke_throttled\"\n ? 429\n : result.error === \"payload_too_large\"\n ? 413\n : 400;\n sendJson(res, status, result);\n return;\n }\n sendJson(res, result.idempotent ? 200 : 202, result);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisStream) {\n const corsOrigin = accessControlAllowOriginValue(req);\n if (!corsOrigin && String(process.env.NODE_ENV || \"\").toLowerCase() === \"production\") {\n const og = String(req.headers?.origin || \"\").trim();\n if (og) {\n console.warn(\"[GATEWAY] genesis SSE: missing Access-Control-Allow-Origin for browser Origin (CORS whitelist mismatch):\", og);\n }\n }\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n res.write(\"retry: 3000\\n\\n\");\n res.write(\": genesis continuity stream\\n\\n\");\n res.write(\"event: genesis\\n\");\n res.write(`data: {\"booted\":true,\"pid\":${process.pid}}\\n\\n`);\n registerGenesisSseClient(res, req);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLatest) {\n const cp = getLatestGenesisSignedCheckpoint();\n if (!cp) {\n sendJson(res, 404, { ok: false, error: \"no_checkpoint_yet\", hint: \"wait_for_seq_interval_or_configure_signing_secret\" });\n return;\n }\n sendJson(res, 200, { ok: true, checkpoint: cp });\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(rhizohRuntime.routes.genesisCheckpointBySeqPrefix)) {\n const tail = pathname.slice(rhizohRuntime.routes.genesisCheckpointBySeqPrefix.length).split(\"/\")[0];\n const seq = parseInt(String(tail || \"\").trim(), 10);\n const out = await genesisCheckpointQueryBySeqV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"checkpoint_not_found\") {\n sendJson(res, 404, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointRange) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryRangeV0(from, to);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"range_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLineage) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const seq = parseInt(String(u.searchParams.get(\"seq\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryLineageV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"lineage_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisContinuityEvents) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const limit = parseInt(String(u.searchParams.get(\"limit\") || \"\").trim(), 10);\n const out = await queryGenesisContinuityEventArchiveV0(from, to, type, limit);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\" || out.error === \"event_archive_disabled\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"archive_file_too_large\") {\n sendJson(res, 413, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplay) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await resolveGenesisReplayRouterV1({\n from: fromSeq,\n to: toSeq,\n type,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayDiff) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const out = await computeGenesisReplayTemporalDiffV1({ from: fromSeq, to: toSeq, type });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEquivalence) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from1 = parseInt(String(u.searchParams.get(\"from1\") || \"\").trim(), 10);\n const to1 = parseInt(String(u.searchParams.get(\"to1\") || \"\").trim(), 10);\n const from2 = parseInt(String(u.searchParams.get(\"from2\") || \"\").trim(), 10);\n const to2 = parseInt(String(u.searchParams.get(\"to2\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await compareGenesisReplayEquivalenceV1({\n from1,\n to1,\n from2,\n to2,\n type,\n includeCheckpoints\n });\n if (\n !out.ok &&\n (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")\n ) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayAnalytics) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayAnalyticsV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEvolution) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const collapseWindows = parseInt(String(u.searchParams.get(\"collapseWindows\") || \"0\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayEvolutionV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n collapseWindows,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/metrics\") {\n res.writeHead(200, { \"Content-Type\": \"text/plain; version=0.0.4; charset=utf-8\" });\n res.end(renderInfraMetricsProm());\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.infraPrometheusMetrics) {\n if (process.env.CASTLE_PROMETHEUS_METRICS !== \"1\") {\n sendJson(res, 404, { ok: false, error: \"prometheus_metrics_disabled\" });\n return;\n }\n res.writeHead(200, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(renderRhizohPrometheusMetrics());\n return;\n }\n if (req.method === \"GET\" && pathname.startsWith(\"/infra/traces/\")) {\n const traceId = decodeURIComponent(pathname.slice(\"/infra/traces/\".length));\n const item = await getTraceById(traceId);\n if (!item) return sendJson(res, 404, { ok: false, error: \"trace_not_found\" });\n return sendJson(res, 200, { ok: true, trace: item });\n }\n if (req.method === \"GET\" && pathname === \"/infra/traces\") {\n const u = new URL(String(req.url || \"/infra/traces\"), `http://${req.headers.host || \"localhost\"}`);\n const sessionId = String(u.searchParams.get(\"sessionId\") || \"\").trim();\n const items = sessionId ? await listTracesBySession(sessionId) : [];\n return sendJson(res, 200, { ok: true, sessionId, traces: items });\n }\n\n /** Presence room mesh (C): JOIN / LEAVE / SNAPSHOT / DELTA / REPLAY (HTTP); SUBSCRIBE = SSE stream. */\n if (req.method === \"POST\" && pathname === \"/presence/mesh/join\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid, clientUid: actor.clientUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/leave\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshLeave(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/snapshot\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n sendJson(res, 200, meshSnapshot(roomUid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/delta\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, MAX_MESSAGE_BYTES);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const seq = meshAppendDelta(roomUid, actor.clientUid, {\n node: body?.node,\n projectionPatch: body?.projectionPatch,\n writerSubject: body?.writerSubject\n });\n sendJson(res, 200, { ok: true, roomUid, seq });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/replay\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const fromNodeId = body?.fromNodeId != null ? String(body.fromNodeId) : \"\";\n const fromSeq = body?.fromSeq;\n if (fromNodeId) {\n sendJson(res, 200, meshReplayFromNodeId(roomUid, fromNodeId));\n return;\n }\n sendJson(res, 200, meshReplayFromSeq(roomUid, fromSeq));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/presence/mesh/subscribe\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const roomUid = String(q.searchParams.get(\"roomUid\") || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n const write = (chunk) => {\n try {\n res.write(chunk);\n } catch {\n /* closed */\n }\n };\n const unsub = meshSubscribeSse(roomUid, write);\n const onClose = () => {\n unsub();\n meshLeave(roomUid, actor.clientUid);\n try {\n res.end();\n } catch {\n /* ignore */\n }\n };\n req.on(\"close\", onClose);\n req.on(\"aborted\", onClose);\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, uid: auth.uid, mode: auth.mode, items: await listConnections(auth.uid) });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const id = await createConnection(auth.uid, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"create_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n await updateConnection(auth.uid, id, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"update_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"DELETE\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n await deleteConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"delete_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections/test\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n if (!checkHttpRateLimit(`llm_conn_test:${auth.uid}`, RL_LLM_CONN_TEST_PER_MIN, 60_000)) {\n return sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n }\n const hasBodyKey = !!String(payload?.apiKey || \"\").trim();\n const result = await queryRhizohLlm(\n {\n message: String(payload?.message || \"Ping from Castle Gateway\"),\n context: payload?.context || { source: \"connection_test\" },\n provider: payload?.provider,\n model: payload?.model,\n apiKey: payload?.apiKey || \"\"\n },\n { allowExternalApiKey: true, llmKeySource: hasBodyKey ? \"auto\" : \"env\" }\n );\n logLlmAccess({\n route: \"/llm/connections/test\",\n uid: auth.uid,\n llmKeyBillingOwner: result.llmKeyBillingOwner,\n llmKeyOrigin: result.llmKeyOrigin,\n provider: result.provider,\n model: result.model,\n connectionId: null\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"connection_test_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAgentIdentities(auth.uid);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerAgentIdentity({\n ownerUid: auth.uid,\n agentId: payload?.agentId,\n role: payload?.role,\n personaSeed: payload?.personaSeed || {},\n capabilityLevel: payload?.capabilityLevel\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/agents/identities/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const owns = await getAgentIdentity(id);\n if (!owns || owns.ownerUid !== auth.uid) return sendJson(res, 403, { ok: false, error: \"forbidden\" });\n const payload = await readHttpJson(req);\n const row = await updateAgentIdentity(id, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/academy/events\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/academy/events\"}`);\n const status = String(url.searchParams.get(\"status\") || \"\");\n const items = await listAcademyEvents({ ownerUid: auth.uid, status, limit: 120 });\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/academy/events\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const ev = await queueAcademyEvent({\n type: payload?.type,\n roomId: payload?.roomId,\n ownerUid: auth.uid,\n participants: payload?.participants || [],\n topic: payload?.topic || \"academy\",\n payload: payload?.payload || {}\n });\n sendJson(res, 200, { ok: true, event: ev });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && String(req.url || \"\").startsWith(\"/academy/events/\") && String(req.url || \"\").endsWith(\"/resolve\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const parts = String(req.url || \"\").split(\"/\").filter(Boolean);\n const eventId = parts[2] || \"\";\n try {\n const payload = await readHttpJson(req);\n const out = await resolveAcademyEvent(eventId, payload || {});\n sendJson(res, 200, { ok: true, event: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_resolve_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/memory\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/memory\"}`);\n const scope = url.searchParams.get(\"scope\") === \"agents\" ? \"agents\" : \"users\";\n const id = String(url.searchParams.get(\"id\") || (scope === \"agents\" ? \"\" : auth.uid));\n const kind = String(url.searchParams.get(\"kind\") || \"\");\n const limit = Number(url.searchParams.get(\"limit\") || 40);\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n sendJson(res, 200, { ok: true, scope, id, kind, items: await listMemories({ scope, id, kind, limit }) });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const profile = await getPersonaGoalMemory(auth.uid);\n sendJson(res, 200, { ok: true, profile });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const profile = await setPersonaGoalMemory({\n uid: auth.uid,\n personaPatch: payload?.personaPatch || {},\n goals: Array.isArray(payload?.goals) ? payload.goals : null,\n preferencesPatch: payload?.preferencesPatch || {}\n });\n sendJson(res, 200, { ok: true, profile });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"profile_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/compact\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const out = await autoCompactMemories({ scope, id });\n sendJson(res, 200, { ok: true, scope, id, ...out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_compact_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/context\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const context = await getMemoryContext({\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n query: String(payload?.query || \"\"),\n limit: Number(payload?.limit || 80)\n });\n sendJson(res, 200, { ok: true, context });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_context_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/place-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const placeName = String(payload?.placeName || \"\").trim();\n if (!placeName) return sendJson(res, 400, { ok: false, error: \"placeName_required\" });\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const persona = String(payload?.waypointPersona || payload?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(placeName)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Place: ${placeName}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Give a concise conversational briefing about this place for a walking-route companion.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-place-brief\", placeName, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `PLACE_BRIEF:${placeName} => ${String(summary.reply || \"\").slice(0, 900)}`,\n tags: [\"place\", \"brief\", \"event-layer\"],\n importance: 0.62,\n meta: { placeName }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"place-brief\",\n text: `${placeName} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: {\n placeName,\n directive: summary.directive,\n lat: hit?.lat ? Number(hit.lat) : null,\n lon: hit?.lon ? Number(hit.lon) : null\n }\n });\n sendJson(res, 200, { ok: true, place: hit || null, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"place_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/route-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const waypoints = Array.isArray(payload?.waypoints) ? payload.waypoints.slice(0, 24) : [];\n const out = [];\n for (const wp of waypoints) {\n const name = String(wp?.name || \"\").trim();\n if (!name) continue;\n const persona = String(wp?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(name)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Waypoint: ${name}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Generate a concise spoken briefing for route waypoint.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-route-brief\", waypoint: name, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n out.push({\n name,\n persona,\n place: hit || null,\n reply: summary.reply,\n directive: summary.directive\n });\n }\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `ROUTE_BRIEF: waypoints=${out.length} ${out.map((x) => `${x.name}:${x.persona}`).join(\" | \")}`.slice(0, 1300),\n tags: [\"route\", \"waypoint\", \"brief\", \"event-layer\"],\n importance: 0.74,\n meta: { count: out.length }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"route-brief\",\n text: out.map((x) => `${x.name}:${String(x.reply || \"\").slice(0, 140)}`).join(\" | \").slice(0, 1400),\n roomId: \"studio-main\",\n meta: {\n waypointCount: out.length,\n waypoints: out.map((x) => ({\n name: x.name,\n persona: x.persona,\n lat: x.place?.lat ? Number(x.place.lat) : null,\n lon: x.place?.lon ? Number(x.place.lon) : null\n }))\n }\n });\n sendJson(res, 200, { ok: true, items: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"route_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/pdf-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const title = String(payload?.title || \"document\");\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const providedText = String(payload?.text || \"\");\n const url = String(payload?.url || \"\");\n let content = providedText;\n if (!content && url) content = await fetchTextFromUrl(url);\n if (!content) return sendJson(res, 400, { ok: false, error: \"text_or_supported_url_required\" });\n const summary = await queryRhizohLlm(\n {\n message: `Summarize this document and provide key points + practical actions.\\nTitle:${title}\\nContent:\\n${content.slice(0, 12000)}`,\n context: { source: \"event-layer-pdf-brief\", title },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `DOC_BRIEF:${title} => ${String(summary.reply || \"\").slice(0, 1000)}`,\n tags: [\"document\", \"brief\", \"library\", \"event-layer\"],\n importance: 0.7,\n meta: { title, url: url || null }\n });\n await addTranscript(auth.uid, {\n source: \"library-brief\",\n eventType: \"pdf-brief\",\n text: `${title} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { title }\n });\n sendJson(res, 200, { ok: true, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/pdf/upload\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req, 8 * 1024 * 1024);\n const job = await enqueuePdfJob({\n uid: auth.uid,\n fileName: payload?.fileName || \"document.pdf\",\n bytesBase64: payload?.contentBase64\n });\n sendJson(res, 200, { ok: true, job });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_upload_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/session\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createPublishSession(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session\",\n text: `Publish session armed: ${row.protocol} ${row.target}`.slice(0, 500),\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: row.id, bridge: row.bridge }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/studio/publish/sessions\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listPublishSessions(auth.uid, 80);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/health-check\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const protocol = String(payload?.protocol || \"WHIP\");\n const target = String(payload?.target || \"\");\n const health = await checkPublishTargetHealth(protocol, target);\n if (payload?.sessionId) {\n await updatePublishSession(auth.uid, String(payload.sessionId), {\n status: health.ok ? \"healthy\" : \"degraded\",\n reconnectPolicy: {\n enabled: true,\n backoffSec: [2, 5, 10, 20],\n nextRetryInSec: health.reconnectInSec || 0,\n maxRetries: 8,\n reason: health.reason\n }\n });\n }\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-health\",\n text: `${protocol} ${target} => ${health.ok ? \"healthy\" : \"degraded\"} (${health.reason})`,\n roomId: \"studio-main\",\n meta: { protocol, target, reconnect: !!health.shouldReconnect }\n });\n sendJson(res, 200, { ok: true, health });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_health_check_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/studio/publish/sessions/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updatePublishSession(auth.uid, id, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session-update\",\n text: `Session ${id} -> ${row.status || \"updated\"}`,\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: id, status: row.status, ingestUrl: row.ingestUrl || \"\" }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/transcripts\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/transcripts\"}`);\n const limit = Math.max(1, Math.min(200, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await listTranscripts(auth.uid, limit);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/transcripts\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await addTranscript(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"transcript_write_failed\" });\n }\n return;\n }\n\n /** E1: broadcast intent → GreenRoom capability → transcript + memory artifact */\n if (req.method === \"POST\" && req.url === \"/studio/capabilities/greenroom\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const traceId = String(payload?.traceId || `TRC-${Date.now().toString(36).toUpperCase()}`);\n const title = String(payload?.title || \"Castle Live Broadcast\").slice(0, 200);\n const intentRaw = String(payload?.intentRaw || \"\").slice(0, 800);\n const audienceEstimate = Math.max(0, Math.min(50_000_000, Number(payload?.audienceEstimate) || 1200));\n const roomId = String(payload?.roomId || \"greenroom-main\");\n const transcriptRow = await addTranscript(auth.uid, {\n source: \"rhizoh\",\n eventType: \"BROADCAST_ROUTED\",\n text: `GreenRoom · ${title}${intentRaw ? `\\nIntent: ${intentRaw}` : \"\"}`,\n roomId,\n meta: {\n ack: \"BROADCAST_ROUTED\",\n capability: \"greenroom\",\n intentType: \"broadcast\",\n traceId,\n audienceEstimate,\n artifact: {\n kind: \"broadcast_card\",\n title,\n traceId,\n roomId,\n createdAt: Date.now()\n }\n }\n });\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n text: `GREENROOM_BROADCAST:${title} trace=${traceId}`,\n tags: [\"broadcast\", \"greenroom\", \"artifact\", \"castle-library\"],\n importance: 0.72,\n kind: \"episodic\",\n meta: { traceId, capability: \"greenroom\", transcriptId: transcriptRow.id }\n });\n sendJson(res, 200, {\n ok: true,\n ack: \"BROADCAST_ROUTED\",\n traceId,\n transcript: transcriptRow,\n replayPath: `/replay/${encodeURIComponent(traceId)}`,\n sharePath: `/replay/${encodeURIComponent(traceId)}`\n });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"greenroom_capability_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/metadata/search\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/metadata/search\"}`);\n const q = String(url.searchParams.get(\"q\") || \"\");\n const limit = Math.max(1, Math.min(300, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await searchMetadata(auth.uid, q, limit);\n sendJson(res, 200, { ok: true, q, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerDevice(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, device: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"device_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const lat = Number(payload?.lat);\n const lon = Number(payload?.lon);\n const speed = Math.max(0, Number(payload?.speed || 0));\n const cfgCenterLat = Number(process.env.CASTLE_GEOFENCE_CENTER_LAT || 41.0082);\n const cfgCenterLon = Number(process.env.CASTLE_GEOFENCE_CENTER_LON || 28.9784);\n const cfgRadiusM = Math.max(100, Number(process.env.CASTLE_GEOFENCE_RADIUS_M || 55000));\n const cfgSpeedCap = Math.max(1, Number(process.env.CASTLE_SPEED_CAP_MPS || 35));\n const dist = Number.isFinite(lat) && Number.isFinite(lon) ? haversineMeters(cfgCenterLat, cfgCenterLon, lat, lon) : 0;\n const geofenceOk = !Number.isFinite(dist) ? true : dist <= cfgRadiusM;\n const speedOk = speed <= cfgSpeedCap;\n const failsafe = !(geofenceOk && speedOk);\n const safety = {\n geofenceOk,\n speedOk,\n failsafe,\n distanceFromCenterM: Number.isFinite(dist) ? dist : null,\n speedCapMps: cfgSpeedCap\n };\n const row = await appendTelemetry(auth.uid, deviceId, {\n lat,\n lon,\n speed,\n battery: payload?.battery,\n mode: payload?.mode || (failsafe ? \"FAILSAFE_HOLD\" : \"ACTIVE\"),\n raw: payload?.raw || {}\n });\n await updateDevice(auth.uid, deviceId, {\n status: failsafe ? \"failsafe\" : \"active\",\n lastTelemetryAt: row.ts,\n safetyState: safety\n });\n if (failsafe) {\n await addTranscript(auth.uid, {\n source: \"robotics-guardrails\",\n eventType: \"failsafe\",\n text: `Device ${deviceId} failsafe triggered geofenceOk=${geofenceOk} speedOk=${speedOk}`,\n roomId: \"studio-main\",\n meta: { deviceId, safety }\n });\n }\n sendJson(res, 200, { ok: true, telemetry: row, safety, command: failsafe ? \"FAILSAFE_HOLD\" : \"CONTINUE\" });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"telemetry_ingest_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listTelemetry(auth.uid, 150);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createAutomation(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"automation-scheduled\",\n text: `Automation scheduled: ${row.title} at ${new Date(row.scheduleAt).toLocaleString()}`,\n roomId: \"studio-main\",\n meta: { automationId: row.id, action: row.action, deviceId: row.deviceId }\n });\n sendJson(res, 200, { ok: true, automation: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"automation_create_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAutomations(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listDevices(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const devs = await listDevices(auth.uid, 500);\n const d = devs.find((x) => x.id === deviceId);\n if (!d) return sendJson(res, 404, { ok: false, error: \"device_not_found\" });\n const action = String(payload?.action || \"\");\n const caps = Array.isArray(d.capabilityProfile) ? d.capabilityProfile : [];\n if (action && caps.length && !caps.includes(action) && !caps.includes(\"*\")) {\n return sendJson(res, 400, { ok: false, error: \"unsupported_action_for_device\", capabilityProfile: caps });\n }\n const cmd = await enqueueCommand(auth.uid, {\n deviceId,\n action: action || \"NOOP\",\n params: payload?.params || {},\n adapter: payload?.adapter || d.adapter || \"websocket\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-command-queue\",\n eventType: \"command-queued\",\n text: `${cmd.deviceId} -> ${cmd.action}`,\n roomId: \"studio-main\",\n meta: { commandId: cmd.id, adapter: cmd.adapter }\n });\n sendJson(res, 200, { ok: true, command: cmd });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listCommands(auth.uid, 200);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/robotics/commands/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updateCommandStatus(auth.uid, id, payload?.status || \"sent\");\n sendJson(res, 200, { ok: true, command: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/rhizoh/bridge\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const result = await runRhizohBrainV2({\n message: String(payload?.message || \"robotics bridge command\"),\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n worldState: payload?.worldState || {},\n deviceState: payload?.deviceState || {},\n intent: \"robotics-mechanics\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-bridge\",\n eventType: \"robotics-command\",\n text: `Bridge -> ${String(result.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { directive: result.directive, agentId: payload?.agentId || \"\" }\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"robotics_bridge_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/planner/graph\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const graph = buildTaskActionGraph(String(payload?.task || \"\"), payload?.context || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"planner-graph\",\n text: `Planner graph built: ${graph.taskTitle} nodes=${graph.nodes.length}`,\n roomId: \"studio-main\",\n meta: { taskTitle: graph.taskTitle, nodeCount: graph.nodes.length }\n });\n sendJson(res, 200, { ok: true, graph });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"planner_graph_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerSocialChannel(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, channel: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_channel_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listSocialChannels(auth.uid, 100);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/broadcast\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const text = String(payload?.text || \"\").slice(0, 1200);\n const channels = await listSocialChannels(auth.uid, 100);\n const enabled = channels.filter((c) => c.status === \"enabled\");\n const deliveries = [];\n for (const c of enabled) {\n const out = await deliverSocialWithRetry(c, text);\n deliveries.push({ id: c.id, platform: c.platform, ...out });\n }\n const okCount = deliveries.filter((d) => d.ok).length;\n await addTranscript(auth.uid, {\n source: \"social-layer\",\n eventType: \"social-broadcast\",\n text: `Broadcast delivered ${okCount}/${enabled.length} channel(s): ${text.slice(0, 240)}`,\n roomId: \"studio-main\",\n meta: { deliveries }\n });\n sendJson(res, 200, { ok: true, sentChannels: okCount, totalChannels: enabled.length, deliveries });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_broadcast_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/ethics/programs\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, ...buildEthicsPrograms() });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands/dispatch\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const all = await listCommands(auth.uid, 200);\n const queued = all.filter((c) => String(c.status || \"queued\") === \"queued\").slice(0, 50);\n const devices = await listDevices(auth.uid, 500);\n const byId = new Map(devices.map((d) => [d.id, d]));\n const results = [];\n for (const cmd of queued) {\n const dev = byId.get(cmd.deviceId);\n if (!dev) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: \"device_not_found\" });\n continue;\n }\n try {\n const dispatched = await dispatchCommandToAdapter(dev, cmd);\n await updateCommandStatus(auth.uid, cmd.id, \"sent\");\n results.push({ id: cmd.id, ok: true, adapter: dispatched.adapter, via: dispatched.via });\n } catch (error) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: String(error?.message || \"dispatch_failed\") });\n }\n }\n await addTranscript(auth.uid, {\n source: \"robotics-command-dispatch\",\n eventType: \"command-dispatch-batch\",\n text: `Dispatch batch size=${results.length} ok=${results.filter((x) => x.ok).length}`,\n roomId: \"studio-main\",\n meta: { results: results.slice(0, 30) }\n });\n sendJson(res, 200, { ok: true, processed: results.length, results });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"dispatch_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/pdf/jobs/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n const job = await getPdfJob(auth.uid, id);\n if (!job) return sendJson(res, 404, { ok: false, error: \"pdf_job_not_found\" });\n sendJson(res, 200, { ok: true, job });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const row = await appendMemory({\n scope,\n id,\n text: payload?.text,\n tags: payload?.tags || [],\n importance: payload?.importance,\n kind: payload?.kind || \"episodic\",\n meta: { uid: auth.uid, ...(payload?.meta || {}) }\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.externalTruth) {\n try {\n if (process.env.RHIZOH_EXTERNAL_TRUTH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_truth:${ip}`, RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n sendJson(res, 200, buildRhizohExternalGroundTruthPayload(req));\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.productOutcome) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const rawBody = await readHttpBodyText(req, 64 * 1024);\n const body = rawBody.trim() ? safeJsonParse(rawBody) : null;\n if (!body || typeof body !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"invalid_json_payload\" });\n return;\n }\n const sigHdr = String(req.headers?.[\"x-rhizoh-outcome-signature\"] || \"\").trim();\n const hmacV = verifyRhizohOutcomeHmac(rawBody, sigHdr);\n if (!hmacV.ok) {\n sendJson(res, 401, { ok: false, error: hmacV.error || \"outcome_hmac_invalid\" });\n return;\n }\n const srcTokHdr = String(req.headers?.[\"x-rhizoh-outcome-source-token\"] || \"\").trim();\n const srcV = verifyRhizohOutcomeSourceToken(body.source, srcTokHdr);\n if (!srcV.ok) {\n sendJson(res, 401, { ok: false, error: srcV.error || \"outcome_source_token_invalid\" });\n return;\n }\n const subjectKey = String(body.subjectKey || \"\").trim().slice(0, 128);\n if (\n subjectKey &&\n !checkHttpRateLimit(\n `rhizoh_product_outcome_subj:${subjectKey}`,\n RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN,\n 60_000\n )\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_subject\" });\n return;\n }\n const result = ingestRhizohProductOutcomeHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n schemaVersion: \"1.1.0\",\n aggregate: result.aggregate,\n integrity: result.integrity\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"outcome_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.productOutcomeAggregate) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome_agg:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const u = new URL(String(req.url || \"/\"), \"http://localhost\");\n const cohortId = u.searchParams.get(\"cohortId\") || \"\";\n const decisionFingerprint = u.searchParams.get(\"decisionFingerprint\") || \"\";\n const limit = u.searchParams.get(\"limit\") || \"50\";\n const rows = listRhizohProductOutcomeAggregates({\n cohortId: cohortId || undefined,\n decisionFingerprint: decisionFingerprint || undefined,\n limit: Number(limit) || 50\n });\n sendJson(res, 200, { ok: true, schemaVersion: \"1.1.0\", rows });\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.externalLossBatch) {\n try {\n if (process.env.RHIZOH_EXTERNAL_LOSS_BATCH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_loss_batch:${ip}`, RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const result = ingestRhizohExternalLossBatchHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, { ok: true, accepted: result.accepted ?? 0 });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"batch_failed\") });\n }\n return;\n }\n\n function moderationAdminOk(req) {\n const expected = String(process.env.CASTLE_MODERATION_ADMIN_KEY || \"\").trim();\n if (!expected) return false;\n const got = String(req.headers[\"x-castle-moderation-key\"] || \"\").trim();\n return got.length > 0 && got === expected;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.opsAbuseReport) {\n try {\n const body = await readHttpJson(req, 32 * 1024);\n const auth = await resolveHttpUser(req);\n const entry = submitAbuseReportV0({\n reporterUid: auth.ok ? auth.uid : null,\n targetUid: body?.targetUid ?? null,\n category: body?.category,\n detail: body?.detail,\n traceId: body?.traceId ?? null\n });\n sendJson(res, 202, { ok: true, reportId: entry.id, status: entry.status });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsModerationQueue) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n queue: listModerationQueueV0(100),\n softBlocked: listSoftBlockedUsersV0()\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-block\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const out = softBlockUserV0(body?.uid, body?.reason);\n sendJson(res, out.ok ? 200 : 400, out);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-unblock\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n sendJson(res, 200, removeSoftBlockV0(body?.uid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsAgentSnapshots) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n const limit = Number(new URL(req.url || \"\", \"http://local\").searchParams.get(\"limit\") || 50);\n sendJson(res, 200, { ok: true, snapshots: listRecentAgentSnapshotsV0(limit) });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.academicObservatoryExport) {\n const auth = await resolveHttpUser(req);\n const out = handleAcademicObservatoryExportGetV0(req, auth);\n sendJson(res, out.status, out.body);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsHardeningStatus) {\n const auth = await resolveHttpUser(req);\n const principal = auth.ok ? `uid:${auth.uid}` : null;\n const gclSnap = await getGlobalCostLedgerSnapshotV0(principal);\n const economicStrategy = evaluateEconomicStrategyL3V0(gclSnap);\n const hardeningTenantId = resolveHardeningTenantIdV0(auth, req);\n const unifiedState = await buildUnifiedStateNarrativeV0({\n tenantId: hardeningTenantId ?? undefined,\n platformScope: !hardeningTenantId,\n principal\n });\n sendJson(res, 200, {\n ok: true,\n schema: \"rhizoh.ops.hardening_status.v0\",\n containment: readAgentContainmentConfigV0(),\n cost: readCostContainmentConfigV0(),\n gcl: {\n backend: await getGclBackendStatusV0(),\n snapshot: gclSnap\n },\n economicStrategyL3: economicStrategy,\n phasedRollout: await getPhasedRolloutStatsV0(),\n tier: readPhasedRolloutTierV0(),\n principalCost: auth.ok ? await getCostContainmentStatsV0(principal) : null,\n stressTaxonomy: verifyStressTaxonomyCoverageV0(),\n stressConflictResolution: verifyStressConflictResolutionV0(),\n resolutionStabilityEnvelope: verifyResolutionStabilityEnvelopeV0(),\n unifiedState: {\n tenantScope: {\n tenantId: unifiedState.tenantScope?.tenantId,\n scope: unifiedState.tenantScope?.scope,\n noGlobalDerivedState: unifiedState.tenantScope?.noGlobalDerivedState\n },\n narrativeFingerprint: unifiedState.narrativeFingerprint,\n screenshotScopeWatermark: unifiedState.screenshotScopeWatermark,\n interpretationSafetyContract: unifiedState.interpretationSafetyContract,\n interpretationUxContract: {\n schema: unifiedState.interpretationUxContract?.schema,\n layerOrder: unifiedState.interpretationUxContract?.layerOrder,\n narrativeNeverVisuallyDominant: unifiedState.interpretationUxContract?.narrativeNeverVisuallyDominant\n },\n governance: {\n narrativeIsNotDecisionLayer: unifiedState.governance.narrativeIsNotDecisionLayer,\n displayOrder: unifiedState.governance.displayOrder,\n trustPosture: unifiedState.governance.trustPosture,\n interpretationUxContract: unifiedState.governance.interpretationUxContract?.schema\n },\n humanOps: {\n decisionTier: unifiedState.humanOps?.humanDecisionScaling?.tier,\n decisionOwner: unifiedState.humanOps?.humanDecisionScaling?.decisionOwner,\n opsRouteId: unifiedState.humanOps?.humanDecisionOpsRunbook?.routing?.routeId,\n slaAckMinutes: unifiedState.humanOps?.humanDecisionOpsRunbook?.sla?.ackMinutes,\n uiDisclaimerRequired: unifiedState.humanOps?.narrativeUiSafety?.displayRules?.requireDisclaimerBanner,\n misreadHighResidual: unifiedState.humanOps?.misreadSimulation?.highResidualCount,\n propagationHighResidual: unifiedState.humanOps?.socialPropagationSimulation?.highResidualCount,\n worstWatermarkSurvivability:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.worstWatermarkSurvivability,\n dominantDistortionSource:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.dominantDistortionSource\n },\n culturalRisk: {\n perceptualRiskClass: unifiedState.culturalRisk?.perceptualRiskClass,\n trustFork: unifiedState.culturalRisk?.trustDynamics?.fork,\n trustDecayScore: unifiedState.culturalRisk?.trustDynamics?.scores?.trustDecay,\n mythologyScore: unifiedState.culturalRisk?.trustDynamics?.scores?.mythology,\n confidenceDetachmentExposure:\n unifiedState.culturalRisk?.trustDynamics?.axes?.confidenceDetachmentExposure\n },\n appliedSystemsLayer: {\n schema: unifiedState.appliedSystemsLayer?.schema,\n researchOutputType: unifiedState.appliedSystemsLayer?.researchGovernance?.outputType,\n roboticsBlockActuation: unifiedState.appliedSystemsLayer?.roboticsGrounding?.blockActuation,\n spiralRumorCount:\n unifiedState.appliedSystemsLayer?.spiralMMOGameKernel?.gameLayer?.rumors?.length\n },\n actionSemanticGovernance: {\n schema: unifiedState.actionSemanticGovernance?.schema,\n highRiskActionCount: unifiedState.actionSemanticGovernance?.highRiskActionCount,\n loopClosed: unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.loopClosed,\n authorityCollapseMitigated:\n unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.risks?.authorityCollapse\n ?.mitigated\n },\n actionContextResolution: {\n schema: unifiedState.actionContextResolution?.schema,\n contextFingerprint: unifiedState.actionContextResolution?.contextFingerprint?.fingerprintHash,\n domainCollision: unifiedState.actionContextResolution?.domainCollision?.detected,\n deployAgentSpiralBlocked:\n unifiedState.actionContextResolution?.actionOverloadingRisk?.deployAgentExample\n ?.spiral_mmo === \"blocked\",\n roboticsActuationEligible:\n unifiedState.actionContextResolution?.roboticsActuationCorrectness?.actuationEligible\n },\n epistemicCoherence: {\n schema: unifiedState.epistemicCoherence?.schema,\n verdict: unifiedState.epistemicCoherence?.systemCoherence?.verdict,\n coherenceScore: unifiedState.epistemicCoherence?.systemCoherence?.score,\n contradictionCount: unifiedState.epistemicCoherence?.crossLayerContradictions?.count,\n operatorDecision: unifiedState.epistemicCoherence?.uxCompression?.operatorDecision,\n headlineTr: unifiedState.epistemicCoherence?.uxCompression?.headline?.tr,\n falseSafetyWarning: Boolean(\n unifiedState.epistemicCoherence?.uxCompression?.falseSafetyWarning\n )\n },\n coherenceAuthorityBoundary: {\n schema: unifiedState.coherenceAuthorityBoundary?.schema,\n bindingValid: unifiedState.coherenceAuthorityBoundary?.eclAuthorityBinding?.valid,\n canExecute: unifiedState.coherenceAuthorityBoundary?.contract?.can_execute,\n mandatoryBannerTr:\n unifiedState.coherenceAuthorityBoundary?.disclaimers?.mandatoryBanner?.tr\n },\n decisionLatencyGovernance: {\n schema: unifiedState.decisionLatencyGovernance?.schema,\n latencyTier: unifiedState.decisionLatencyGovernance?.latencyTier,\n inflationScore: unifiedState.decisionLatencyGovernance?.latencyInflation?.score,\n fastPathEligible: unifiedState.decisionLatencyGovernance?.fastPath?.eligible,\n primaryActionId: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.primaryActionId,\n ackSlaMinutes: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.ackSlaMinutes,\n certaintyCap: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.certaintyCap,\n uncertaintyItems:\n unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.uncertaintyEnvelope?.itemCount\n },\n decisionPacketUncertaintyBoundary: {\n schema: unifiedState.decisionPacketUncertaintyBoundary?.schema,\n invariantValid: unifiedState.decisionPacketUncertaintyBoundary?.invariantCheck?.valid,\n centralInvariant: unifiedState.decisionPacketUncertaintyBoundary?.centralInvariant\n },\n epistemicDecisionPacing: {\n schema: unifiedState.epistemicDecisionPacing?.schema,\n queueSaturationTier:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation?.tier,\n operatorProcessingLatency:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation\n ?.operatorProcessingLatency,\n postureWindowCount:\n unifiedState.epistemicDecisionPacing?.temporaryStaticPostureWindows\n ?.temporaryStaticPostureWindows?.windowCount,\n executionBoundariesValid:\n unifiedState.epistemicDecisionPacing?.executionBoundaries?.valid,\n grantsExecutionApproval: unifiedState.epistemicDecisionPacing?.grantsExecutionApproval\n },\n epistemicTemporalCoherence: {\n schema: unifiedState.epistemicTemporalCoherence?.schema,\n allInvariantsValid: unifiedState.epistemicTemporalCoherence?.allInvariantsValid,\n temporalAlignmentValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.temporalAlignment?.valid,\n crossWindowGuardValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard?.valid,\n delayedTruthValid: unifiedState.epistemicTemporalCoherence?.invariants?.delayedTruthRule?.valid,\n reconciliationRequired:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard\n ?.reconciliation?.required\n },\n realityDriftObserver: {\n schema: unifiedState.realityDriftObserver?.schema,\n driftActive: unifiedState.realityDriftObserver?.driftObserverSummary?.active,\n misapprehensionShapeCount:\n unifiedState.realityDriftObserver?.misapprehensionShapeCatalog?.shapeCount,\n criticalShapeCount:\n unifiedState.realityDriftObserver?.driftObserverSummary?.criticalShapeCount,\n roboticsMismatchActive:\n unifiedState.realityDriftObserver?.roboticsFeedbackMismatch?.active\n },\n driftCausality: {\n schema: unifiedState.driftCausality?.schema,\n shapesWithCausalExplanation: unifiedState.driftCausality?.rdolLinkage?.shapesWithCausalExplanation,\n activeDomainCount: unifiedState.driftCausality?.causalitySummary?.activeDomainCount,\n propagationCausalityActive: unifiedState.driftCausality?.causalDomains?.propagation?.active\n },\n productReadiness: unifiedState.productReadiness,\n stateLayers: {\n raw: unifiedState.stateLayers.raw,\n derived: {\n systemState: unifiedState.stateLayers.derived.systemState,\n narrative: unifiedState.stateLayers.derived.narrative,\n validation: unifiedState.stateLayers.derived.validation\n },\n policy: {\n binding: unifiedState.stateLayers.policy.binding,\n decisionOwner: unifiedState.stateLayers.policy.decisionOwner,\n prohibitedActions: unifiedState.stateLayers.policy.prohibitedActions\n }\n },\n systemState: unifiedState.systemState,\n interpretation: unifiedState.interpretation,\n validation: {\n trustPosture: unifiedState.validation.trustPosture,\n adjustedConfidence: unifiedState.validation.adjustedConfidence,\n agreement: unifiedState.validation.confidenceDecomposition.agreement,\n divergenceScore: unifiedState.validation.divergence.divergenceScore,\n validated: unifiedState.validation.divergence.validated,\n uncertaintyTags: unifiedState.validation.uncertainty.tags.map((t) => t.id),\n flags: unifiedState.validation.divergence.flags.map((f) => f.id)\n },\n compression: unifiedState.compression\n }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3) {\n try {\n const payload = await readHttpJson(req, 6 * 1024 * 1024);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (\n !checkHttpRateLimit(`rhizoh_voice_transcribe:${rlKey}`, RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN, 60_000)\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const voiceEnv = rhizohVoiceTranscribeEnvV3();\n if (!voiceEnv.any) {\n sendJson(res, 503, {\n ok: false,\n error: \"voice_asr_not_configured\",\n env: voiceEnv\n });\n return;\n }\n const result = await runRhizohVoiceTranscribeV3(payload);\n sendJson(res, result.ok ? 200 : 422, {\n ...result,\n traceId: String(payload?.traceId || \"\"),\n atMs: Date.now()\n });\n } catch (error) {\n sendJson(res, 500, {\n ok: false,\n error: \"voice_transcribe_failed\",\n detail: String(error?.message || error)\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === rhizohRuntime.routes.rhizohLlm) {\n try {\n const payload = await readHttpJson(req);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n logRhizohHealth(\"gateway_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: String(payload?.traceId || \"\"),\n auth: auth.ok ? \"ok\" : \"anon\",\n ip\n });\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (!checkHttpRateLimit(`rhizoh_llm:${rlKey}`, RL_RHIZOH_LLM_PER_MIN, 60_000)) {\n const rlTaxonomy = classifyStressResponseV0({ code: \"rate_limit_exceeded\" });\n return sendJson(res, 429, {\n ok: false,\n error: \"rate_limit_exceeded\",\n reply: rlTaxonomy.userMessageTr || \"İstek sınırı aşıldı. Kısa süre sonra tekrar deneyin.\",\n stressClass: rlTaxonomy.stressClass,\n responseAction: rlTaxonomy.responseAction,\n stressMatrix: rlTaxonomy.matrix,\n stressInterpretable: rlTaxonomy.interpretable,\n stressConfidence: rlTaxonomy.stressConfidence,\n actionConfidence: rlTaxonomy.actionConfidence,\n actionSoftened: rlTaxonomy.actionSoftened,\n responseActionStrict: rlTaxonomy.responseActionStrict,\n actionInterpretable: rlTaxonomy.actionInterpretable,\n conflictResolution: rlTaxonomy.conflictResolution,\n directive: \"NONE\"\n });\n }\n\n const requireExplicit = process.env.CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE === \"1\";\n const keyMode = normalizeClientLlmKeySource(payload?.llmKeySource ?? payload?.keySource);\n if (keyMode === null) {\n return sendJson(res, 400, {\n ok: false,\n error: \"invalid_llm_key_source\",\n reply: \"llmKeySource geçersiz. Kullanın: env | user_connection | auto\",\n directive: \"NONE\"\n });\n }\n if (requireExplicit && keyMode === \"auto\") {\n return sendJson(res, 400, {\n ok: false,\n error: \"llm_key_source_required\",\n reply: \"Sunucu CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE=1: istekte llmKeySource (env veya user_connection) zorunlu.\",\n directive: \"NONE\"\n });\n }\n\n const conn = auth.ok ? await resolveConnection(auth.uid, String(payload?.connectionId || \"\")) : null;\n\n if (keyMode === \"user_connection\") {\n if (!auth.ok) {\n return sendJson(res, 401, {\n ok: false,\n error: \"llm_auth_required_for_user_connection\",\n reply: \"Kayıtlı LLM bağlantısı için giriş gerekli.\",\n directive: \"NONE\"\n });\n }\n if (!conn?.apiKey) {\n return sendJson(res, 400, {\n ok: false,\n error: \"user_llm_connection_required\",\n reply: \"Geçerli bir LLM bağlantısı yok; Studio’da bağlantı ekleyin veya llmKeySource=env kullanın.\",\n directive: \"NONE\"\n });\n }\n }\n\n const safePayload = { ...(payload || {}) };\n delete safePayload.apiKey;\n delete safePayload.llmKeySource;\n delete safePayload.keySource;\n\n let resolvedProvider = payload?.provider;\n let resolvedModel = payload?.model;\n let connApiKey = \"\";\n\n if (keyMode === \"user_connection\") {\n resolvedProvider = conn.provider;\n resolvedModel = conn.model;\n connApiKey = conn.apiKey || \"\";\n } else if (keyMode === \"env\") {\n connApiKey = \"\";\n } else {\n resolvedProvider = conn?.provider || payload?.provider;\n resolvedModel = conn?.model || payload?.model;\n connApiKey = conn?.apiKey || \"\";\n }\n\n const { result, traceId, turnLatencyMs, spinePhases, sampledTrace } = await rhizohGatewayTurn({\n safePayload,\n auth,\n keyMode,\n conn,\n resolvedProvider,\n resolvedModel,\n connApiKey\n });\n logRhizohHealth(\"llm_response\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId,\n turnLatencyMs: Number(turnLatencyMs || 0),\n keyMode\n });\n sendJson(res, 200, {\n ...result,\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n traceId,\n turnLatencyMs,\n ...(spinePhases ? { spinePhases } : {}),\n ...(sampledTrace !== undefined ? { sampledTrace } : {})\n });\n } catch (error) {\n const msg = String(error?.message || \"\");\n const code = error?.code || \"\";\n let status = 500;\n if (msg === \"rate_limit_exceeded\") status = 429;\n else if (\n code === \"cost_hard_limit\" ||\n code === \"phased_rollout_capacity\" ||\n error?.containment === true ||\n String(code).startsWith(\"agent_\")\n ) {\n status = 429;\n } else if (code === \"user_soft_blocked\" || code === \"prompt_abuse_detected\") status = 403;\n else if ([\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(code) || msg.includes(\"missing_api_key_for_\"))\n status = 400;\n let rhizohFailureKind = \"provider_error\";\n if (msg === \"rate_limit_exceeded\" || status === 429) rhizohFailureKind = \"rate_limit\";\n else if (status === 403) rhizohFailureKind = \"policy_block\";\n else if (status === 400) rhizohFailureKind = \"client_config\";\n else if (msg.startsWith(\"provider_http_\")) {\n const st = Number(msg.replace(\"provider_http_\", \"\"));\n if (st === 408 || st === 504) rhizohFailureKind = \"timeout\";\n else rhizohFailureKind = \"provider_error\";\n } else if (/timeout|timed out|ETIMEDOUT/i.test(msg)) rhizohFailureKind = \"timeout\";\n\n const providerHttpMatch = msg.match(/^provider_http_(\\d{3})$/);\n const stressTaxonomy =\n error?.stressClass != null\n ? {\n stressClass: error.stressClass,\n responseAction: error.responseAction,\n stressMatrix: error.stressMatrix,\n stressInterpretable: error.stressInterpretable !== false,\n stressConfidence: error.stressConfidence,\n actionConfidence: error.actionConfidence,\n actionSoftened: error.actionSoftened,\n responseActionStrict: error.responseActionStrict,\n actionInterpretable: error.actionInterpretable,\n conflictResolution: error.conflictResolution,\n stressSecondary: error.stressSecondary,\n responseActions: error.responseActions\n }\n : (() => {\n const t = classifyStressResponseV0({\n code: code || msg,\n rhizohFailureKind,\n providerHttpStatus: providerHttpMatch\n ? Number(providerHttpMatch[1])\n : null\n });\n return {\n stressClass: t.stressClass,\n responseAction: t.responseAction,\n stressMatrix: t.matrix,\n stressInterpretable: t.interpretable,\n stressConfidence: t.stressConfidence,\n actionConfidence: t.actionConfidence,\n actionSoftened: t.actionSoftened,\n responseActionStrict: t.responseActionStrict,\n actionInterpretable: t.actionInterpretable,\n conflictResolution: t.conflictResolution,\n stressSecondary: t.stressSecondary,\n responseActions: t.responseActions\n };\n })();\n sendJson(res, status, {\n ok: false,\n rhizohFailureKind,\n ...(providerHttpMatch ? { providerHttpStatus: Number(providerHttpMatch[1]) } : {}),\n error: code || msg || \"rhizoh_llm_failed\",\n reply:\n error?.reply ||\n (status === 500 ? \"Rhizoh bağlantısı geçici olarak kesildi.\" : msg),\n directive: error?.directive || \"NONE\",\n ...stressTaxonomy\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicSeal) {\n try {\n if (!EPISTEMIC_SEAL_SECRET) {\n sendJson(res, 503, { ok: false, error: \"epistemic_seal_disabled\" });\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`epistemic_seal:${ip}`, RL_EPISTEMIC_SEAL_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const truth = body.truth_contract;\n if (!truth || typeof truth !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"truth_contract_required\" });\n return;\n }\n const sealBody = {\n truth_contract: truth,\n runtime_hash: String(body.runtime_hash || \"\"),\n model_route: {\n provider: body.model_route?.provider ?? null,\n model: body.model_route?.model ?? null\n },\n memory_digest: String(body.memory_digest || \"\"),\n world_snapshot_hash: String(body.world_snapshot_hash || \"\"),\n timestamp: Number(body.timestamp) || Date.now()\n };\n const canonical = canonicalEpistemicSealString(sealBody);\n const { hash, signature } = hashAndSignEpistemicSeal(canonical, EPISTEMIC_SEAL_SECRET);\n recordGenesisEpistemicSealIssued(hash);\n const attestation = {\n schema: EPISTEMIC_SEAL_SCHEMA,\n algorithm: \"SHA-256+HMAC-SHA256\",\n keyHint: process.env.CASTLE_EPISTEMIC_SEAL_SECRET ? \"CASTLE_EPISTEMIC_SEAL_SECRET\" : \"CASTLE_GATEWAY_TOKEN\",\n issuedAt: Date.now(),\n gateway: { service: \"castle-gateway\", listenPort: PORT }\n };\n sendJson(res, 200, {\n ok: true,\n hash,\n signature,\n attestation,\n canonicalBytes: Buffer.byteLength(canonical, \"utf8\")\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"seal_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicLogsBatch) {\n try {\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const auth = await resolveHttpUser(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`epistemic_logs:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 120) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const persisted = await persistEpistemicLedgerBatch(auth.uid, entries);\n recordGenesisEpistemicLedgerPersisted(persisted.persisted);\n // Internal cognition layer: forecast stays gateway/ledger-only (no client exposure).\n try {\n await persistEpistemicForecastBatch(auth.uid, persisted.normalized || []);\n } catch {\n /* forecast failures must not break primary ledger ingest */\n }\n sendJson(res, 200, {\n ok: true,\n persisted: persisted.persisted,\n mode: persisted.mode,\n latest: persisted.normalized?.[persisted.normalized.length - 1] || null\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"epistemic_log_ingest_failed\") });\n }\n return;\n }\n\n sendJson(res, 404, { ok: false, error: \"not_found\" });\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\nspiralWssForGenesis = wss;\nconst orchestrator = createOrchestrator();\nconst clientStats = new Map();\nlet broadcasterClientId = null;\nconst spiralState = {\n rooms: [{ id: \"room-default\", name: \"Spiral Default Arena\" }],\n activeByClient: new Map(),\n characters: new Map() // id -> {id,name,role,roomId,source}\n};\n\n/** Castle social rooms — keyed by `castleRoomKey` from {@link WS_MESSAGE.CASTLE_SOCIAL_PULSE}. */\nconst castleSocialRoomState = new Map();\nconst CASTLE_SOCIAL_MEMBER_TTL_MS = 90_000;\nconst CASTLE_SOCIAL_MAX_PULSE_BYTES = 8192;\n\nfunction removeCastleSocialClientFromAllRooms(clientId) {\n const cid = String(clientId || \"\");\n if (!cid) return;\n for (const room of castleSocialRoomState.values()) {\n room.members.delete(cid);\n }\n}\n\nfunction buildCastleSocialRoomPayload(roomKey) {\n const room = castleSocialRoomState.get(roomKey);\n if (!room) return null;\n const now = Date.now();\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const roster = [...room.members.values()].map((m) => ({\n userId: m.userId,\n lastMs: m.lastMs,\n ...m.sp\n }));\n const peerFeeds =\n room.walPeers && room.walPeers.size\n ? [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }))\n : [];\n return { castleRoomKey: roomKey, seq: room.seq, roster, peerFeeds, ts: now };\n}\n\nconst CASTLE_WAL_PEER_MAX_FEED_BYTES = 48 * 1024;\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {{ ok: boolean, code?: string, reason?: string }} gate\n */\nfunction rejectWalPeerIngressV0(socket, gate) {\n const code = gate.code || \"wal_peer_rejected\";\n recordWalPeerRejectV0(code);\n recordSubstrateOtelEventV0(\"wal_peer_reject\", { code, reason: String(gate.reason || \"\").slice(0, 120) });\n socket.send(\n JSON.stringify(\n createEnvelope(WS_MESSAGE.ERROR, {\n error: gate.reason || \"WAL peer ingress rejected.\",\n code\n })\n )\n );\n}\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {Record<string, unknown> | null | undefined} walPeerFeed\n * @returns {boolean}\n */\nfunction gateWalPeerIngressV0(socket, walPeerFeed) {\n const authGate = assertWalPeerSocketAuthorityV0(socket);\n if (!authGate.ok) {\n rejectWalPeerIngressV0(socket, authGate);\n return false;\n }\n const sigGate = validateWalPeerFeedSignatureV0(\n walPeerFeed && typeof walPeerFeed === \"object\" ? walPeerFeed : {}\n );\n if (!sigGate.ok) {\n rejectWalPeerIngressV0(socket, sigGate);\n return false;\n }\n return true;\n}\n\nfunction handleCastleWalPeerFeed(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_WAL_PEER_MAX_FEED_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_WAL_PEER_FEED too large.\" })));\n return;\n }\n const walPeerFeed = p.walPeerFeed && typeof p.walPeerFeed === \"object\" ? p.walPeerFeed : {};\n if (!gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const castleRoomKey = String(p.castleRoomKey || \"wal:convergence\").slice(0, 64);\n const castleId = String(p.castleId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId,\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: room.members.get(socket.clientId)?.sp ?? {}\n });\n const peerFeeds = [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }));\n const body = {\n castleRoomKey,\n seq: room.seq,\n peerFeeds,\n ts: now\n };\n const env = createEnvelope(WS_MESSAGE.CASTLE_WAL_PEER_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys(), ...room.walPeers.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction handleCastleSocialPulse(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_SOCIAL_MAX_PULSE_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_SOCIAL_PULSE too large.\" })));\n return;\n }\n const castleRoomKey = String(p.castleRoomKey || \"default\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n const sp = p.socialPulse && typeof p.socialPulse === \"object\" ? p.socialPulse : {};\n const walPeerFeed =\n p.walPeerFeed && typeof p.walPeerFeed === \"object\"\n ? p.walPeerFeed\n : sp.walPeerFeed && typeof sp.walPeerFeed === \"object\"\n ? sp.walPeerFeed\n : null;\n if (walPeerFeed && !gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const spNext = walPeerFeed ? { ...sp, walPeerFeed } : { ...sp };\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: spNext\n });\n if (walPeerFeed) {\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId: String(p.castleId || userId).slice(0, 64),\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n }\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const body = buildCastleSocialRoomPayload(castleRoomKey);\n if (!body) return;\n const env = createEnvelope(WS_MESSAGE.CASTLE_SOCIAL_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcast(msgObj) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcastState() {\n broadcast(\n createEnvelope(WS_MESSAGE.BROADCAST_STATE, {\n broadcasterClientId,\n viewers: [...wss.clients].filter((c) => c.readyState === 1).length\n })\n );\n}\n\nfunction currentSpiralStatePayload() {\n return {\n rooms: spiralState.rooms,\n characters: [...spiralState.characters.values()]\n };\n}\n\nfunction broadcastSpiralState() {\n broadcast(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload()));\n}\n\nfunction ensureRoom(roomId, fallbackName = null) {\n if (!roomId) return spiralState.rooms[0].id;\n const exists = spiralState.rooms.find((r) => r.id === roomId);\n if (exists) return exists.id;\n const created = { id: roomId, name: fallbackName || `Spiral-${roomId.slice(-4)}` };\n spiralState.rooms.push(created);\n return created.id;\n}\n\nfunction syncSpiralCharactersWithWorld(snapshot) {\n const agents = snapshot?.agents || [];\n for (const a of agents) {\n if (!spiralState.characters.has(a.id)) {\n spiralState.characters.set(a.id, {\n id: a.id,\n name: a.id,\n role: \"agent\",\n roomId: spiralState.rooms[0].id,\n source: \"world\"\n });\n }\n }\n}\n\nfunction broadcastExcept(msgObj, exceptSocket) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client !== exceptSocket && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction validateInputFrame(payload, socket) {\n const stats = clientStats.get(socket.clientId) || { lastFrame: 0, windowStart: Date.now(), msgCount: 0 };\n const now = Date.now();\n if (now - stats.windowStart > 5000) {\n stats.windowStart = now;\n stats.msgCount = 0;\n }\n stats.msgCount += 1;\n clientStats.set(socket.clientId, stats);\n if (stats.msgCount > 120) return \"Rate limit exceeded.\";\n\n if (!payload || !Number.isInteger(payload.frame)) return \"frame must be integer.\";\n if (payload.frame <= stats.lastFrame) return \"frame must be monotonic.\";\n const commands = Array.isArray(payload.commands) ? payload.commands : [];\n if (commands.length > 8) return \"Too many commands in one frame.\";\n for (const cmd of commands) {\n if (!cmd || typeof cmd.command !== \"string\") return \"Invalid command format.\";\n if (\n cmd.command !== \"SPAWN_AGENT\" &&\n cmd.command !== \"LIST_CASTLES\" &&\n cmd.command !== \"SPAWN_ENTITY\" &&\n cmd.command !== \"CREATE_CASTLE\"\n ) return \"Unsupported command.\";\n }\n stats.lastFrame = payload.frame;\n clientStats.set(socket.clientId, stats);\n return null;\n}\n\nfunction validateSignalPayload(payload) {\n if (!payload || typeof payload !== \"object\") return \"Invalid signaling payload.\";\n const signalType = payload.signalType;\n if (![\"OFFER\", \"ANSWER\", \"ICE\"].includes(signalType)) return \"Invalid signal type.\";\n if (typeof payload.to !== \"string\" || payload.to.length < 3) return \"Invalid signal destination.\";\n if (signalType !== \"ICE\" && JSON.stringify(payload.sdp || {}).length > 16 * 1024) return \"SDP too large.\";\n if (signalType === \"ICE\" && JSON.stringify(payload.candidate || {}).length > 6 * 1024) return \"ICE too large.\";\n return null;\n}\n\nfunction sanitizeRawMessage(raw) {\n if (!raw) return null;\n const text = raw.toString();\n if (Buffer.byteLength(text, \"utf8\") > MAX_MESSAGE_BYTES) return null;\n return text;\n}\n\nwss.on(\"connection\", async (socket, req) => {\n const origin = req?.headers?.origin || \"\";\n if (ALLOWED_ORIGINS.length && !ALLOWED_ORIGINS.includes(origin)) {\n socket.close(1008, \"Origin not allowed\");\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const path = req?.url || \"\";\n const query = new URL(`ws://localhost${path}`).searchParams;\n const token = query.get(\"token\");\n if (!token || token !== REQUIRED_GATEWAY_TOKEN) {\n socket.close(1008, \"Invalid gateway token\");\n return;\n }\n }\n if (REQUIRE_AUTH) {\n const result = await verifyClientToken(req);\n if (!result.ok) {\n const isLocal = origin.includes(\"localhost\") || origin.includes(\"127.0.0.1\") || !origin;\n if (!(ALLOW_DEV_ANON && isLocal)) {\n socket.close(1008, result.reason);\n return;\n }\n socket.auth = { ok: true, kind: \"dev-anon\", user: { uid: \"dev-anon\" } };\n } else {\n socket.auth = result;\n }\n }\n\n socket.clientId = `c-${Math.random().toString(36).slice(2, 8)}`;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.HELLO, { message: \"Castle Gateway online.\", clientId: socket.clientId })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.WORLD_SNAPSHOT, orchestrator.worldSnapshot())));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.BROADCAST_STATE, { broadcasterClientId, viewers: [...wss.clients].filter((c) => c.readyState === 1).length })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload())));\n broadcastExcept(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }), socket);\n\n socket.on(\"message\", (raw) => {\n const safeRaw = sanitizeRawMessage(raw);\n if (!safeRaw) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Message too large or invalid.\" })));\n return;\n }\n const parsed = safeJsonParse(safeRaw);\n if (!parsed?.type) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid message.\" })));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND_TEXT) {\n const canonical = parseTextToCommand(parsed.payload?.text);\n if (!canonical) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Unknown command text.\" })));\n return;\n }\n const result = orchestrator.applyCommand(canonical);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND) {\n const result = orchestrator.applyCommand(parsed.payload);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n }\n\n if (parsed.type === WS_MESSAGE.INPUT_FRAME) {\n const err = validateInputFrame(parsed.payload, socket);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const frame = parsed.payload.frame;\n const commands = Array.isArray(parsed.payload?.commands) ? parsed.payload.commands : [];\n orchestrator.queueInputFrame(socket.clientId, frame, commands);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, queued: true, frame })));\n }\n\n if (parsed.type === WS_MESSAGE.SIGNAL) {\n const payload = parsed.payload || {};\n const err = validateSignalPayload(payload);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const signalType = payload.signalType;\n const to = payload.to;\n for (const client of wss.clients) {\n if (client.clientId === to && client.readyState === 1) {\n client.send(JSON.stringify(createEnvelope(WS_MESSAGE.SIGNAL, { ...payload, from: socket.clientId })));\n break;\n }\n }\n }\n\n if (parsed.type === WS_MESSAGE.DRONE_TELEMETRY) {\n const payload = parsed.payload || {};\n const json = JSON.stringify(payload);\n if (Buffer.byteLength(json, \"utf8\") > 48 * 1024) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"DRONE_TELEMETRY payload too large.\" })));\n return;\n }\n broadcastExcept(createEnvelope(WS_MESSAGE.DRONE_TELEMETRY, { ...payload, relayFrom: socket.clientId }), socket);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_SOCIAL_PULSE) {\n handleCastleSocialPulse(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_WAL_PEER_FEED) {\n handleCastleWalPeerFeed(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.OPEN_DATA_QUERY) {\n queryOpenData(parsed.payload)\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: true, result })));\n })\n .catch((e) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: false, error: e.message })));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_QUERY) {\n runRhizohBrainV2({\n ...(parsed.payload || {}),\n uid: socket?.auth?.user?.uid || \"unknown\"\n })\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, result)));\n })\n .catch(async () => {\n const fallback = await runRhizohBrain(parsed.payload || {}).catch(() => ({\n ok: false,\n reply: \"Rhizoh temporary disturbance. Habitat remains stable.\"\n }));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, fallback)));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.BROADCAST_REGISTER) {\n const role = String(parsed.payload?.role || \"\");\n if (role === \"GENESIS_BROADCAST_AGENT\") {\n broadcasterClientId = socket.clientId;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, registered: role })));\n broadcastState();\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.STUDIO_CUE) {\n // Only registered broadcaster can push global cues.\n if (!broadcasterClientId || socket.clientId !== broadcasterClientId) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Only broadcaster agent can emit STUDIO_CUE.\" })));\n return;\n }\n const cue = parsed.payload || {};\n if (typeof cue !== \"object\") {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid STUDIO_CUE payload.\" })));\n return;\n }\n broadcast(createEnvelope(WS_MESSAGE.STUDIO_CUE, cue));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_JOIN_ROOM) {\n const requested = String(parsed.payload?.roomId || \"\").trim();\n const requestedName = String(parsed.payload?.roomName || \"\").trim();\n const roomId = ensureRoom(requested || `room-${Math.random().toString(36).slice(2, 7)}`, requestedName || null);\n spiralState.activeByClient.set(socket.clientId, roomId);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, roomId })));\n broadcastSpiralState();\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_CREATE_CHARACTER) {\n const name = String(parsed.payload?.name || \"\").trim().slice(0, 32) || `AI-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;\n const role = String(parsed.payload?.role || \"agent\").trim().slice(0, 24) || \"agent\";\n const roomId = ensureRoom(\n String(parsed.payload?.roomId || spiralState.activeByClient.get(socket.clientId) || spiralState.rooms[0].id),\n null\n );\n\n // Character creation always maps to an authoritative world agent.\n const result = orchestrator.applyCommand({\n command: COMMAND.SPAWN_AGENT,\n params: { targetCastleId: \"castle-01\" }\n });\n if (!result?.ok || !result?.data?.id) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Character creation failed.\" })));\n return;\n }\n const id = result.data.id;\n spiralState.characters.set(id, { id, name, role, roomId, source: \"spawned\" });\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, characterId: id, roomId })));\n broadcastSpiralState();\n return;\n }\n });\n\n socket.on(\"close\", () => {\n if (socket.clientId === broadcasterClientId) broadcasterClientId = null;\n clientStats.delete(socket.clientId);\n spiralState.activeByClient.delete(socket.clientId);\n removeCastleSocialClientFromAllRooms(socket.clientId);\n broadcast(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }));\n broadcastState();\n });\n});\n\nsetInterval(() => {\n void (async () => {\n try {\n const { frame, snapshot, delta, packedDelta } = orchestrator.tick(1 / 20);\n syncSpiralCharactersWithWorld(snapshot);\n broadcast(createEnvelope(WS_MESSAGE.NETWORK_TICK, { frame }));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_TICK, snapshot));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA, delta));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA_PACKED, packedDelta));\n broadcastSpiralState();\n await runAcademyEventTick();\n const executed = await tickAutomations(Date.now());\n for (const a of executed) {\n await addTranscript(a.uid, {\n source: \"planner\",\n eventType: \"automation-executed\",\n text: `Automation executed: ${a.title} (${a.action})`,\n roomId: \"studio-main\",\n meta: { automationId: a.id, action: a.action, deviceId: a.deviceId || \"\" }\n });\n }\n } catch (e) {\n console.error(\"[GATEWAY] world tick / automations loop error:\", String(e?.message || e));\n }\n })();\n}, 50);\n\n(async () => {\n await rhizohRuntime.telemetry.initOpenTelemetry();\n if (String(process.env.CASTLE_GENESIS_DISK_PERSIST ?? \"\").trim() === \"\") {\n process.env.CASTLE_GENESIS_DISK_PERSIST = \"1\";\n }\n const genesisDiskBoot = await hydrateGenesisContinuityPersistenceBootV0();\n if (!genesisDiskBoot.ok) {\n console.warn(\"[GATEWAY] genesis disk hydrate failed; continuing with in-memory genesis continuity\");\n }\n httpServer.listen(PORT, () => {\n startGenesisCanonicalClock();\n installGenesisCheckpointSurfaceGetter(buildGenesisRuntimeSurfacePayloadLive);\n setGenesisContinuityAfterPublishHook((seq) => {\n noteGenesisCheckpointSeqCommitted(seq);\n noteGenesisContinuitySeqAcceptedV0(seq);\n });\n startGenesisContinuityInfraSampler(buildGenesisRuntimeSurfacePayloadLive, 2000);\n logProductionObservatorySurfaceGuardsV0();\n logGatewaySubstrateAuthorityGuardsV0();\n console.log(`[GATEWAY] ws/http://localhost:${PORT}`);\n console.log(\n `[GATEWAY] genesis continuity: GET ${rhizohRuntime.routes.genesisRuntime} | POST ${rhizohRuntime.routes.genesisIngress} | SSE ${rhizohRuntime.routes.genesisStream} | replay ${rhizohRuntime.routes.genesisReplay} | diff ${rhizohRuntime.routes.genesisReplayDiff} | equiv ${rhizohRuntime.routes.genesisReplayEquivalence} | analytics ${rhizohRuntime.routes.genesisReplayAnalytics} | evolution ${rhizohRuntime.routes.genesisReplayEvolution}`\n );\n if (String(process.env.CASTLE_GENESIS_OBSERVATORY_LIVE_V01 ?? \"\").trim() === \"1\") {\n console.log(\"GENESIS OBSERVATORY LIVE v0.1 — READ ONLY PHASE STARTED\");\n }\n });\n})().catch((e) => {\n console.error(\"[GATEWAY] fatal boot (before or during listen):\", e);\n process.exit(1);\n});\n"}, "0f4f40bfbaa561c2003d5824fdbbdb9bf1616631": {"t": 1785754168.0908387, "n": 152872, "h": "3f569bc83c8a076d", "c": "import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport dotenv from \"dotenv\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconsole.log(\"🔥 [GENESIS_BOOT] entry file:\", __filename);\nconsole.log(\"🔥 [GENESIS_BOOT] node env:\", process.env.NODE_ENV);\nconsole.log(\"🔥 [GENESIS_BOOT] pid:\", process.pid);\nconsole.log(\"🧬 [GENESIS_BOOT] cwd:\", process.cwd());\nconsole.log(\"🧬 [GENESIS_BOOT] import.meta.url:\", import.meta.url);\ndotenv.config({ path: path.join(__dirname, \"..\", \".env\") });\ndotenv.config({ path: path.join(__dirname, \"..\", \".env.local\"), override: true });\n\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport { createServer } from \"node:http\";\nimport { createOrchestrator } from \"@castle/orchestrator\";\nimport { parseTextToCommand } from \"@castle/command-dsl\";\nimport { WS_MESSAGE, COMMAND, createEnvelope, safeJsonParse } from \"@castle/protocol\";\nimport { queryOpenData } from \"./openData.js\";\nimport { verifyClientToken } from \"./auth.js\";\nimport { runRhizohBrain } from \"./rhizohBrain.js\";\nimport { queryRhizohLlm } from \"./rhizohLlmGateway.js\";\nimport { rhizohGatewayTurn } from \"./rhizohGatewayTurn.js\";\nimport {\n RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n runRhizohVoiceTranscribeV3,\n rhizohVoiceTranscribeEnvV3\n} from \"./rhizohVoiceTranscribeV3.js\";\nimport {\n meshAppendDelta,\n meshContinuityAggregate,\n meshJoin,\n meshLeave,\n meshReplayFromNodeId,\n meshReplayFromSeq,\n meshSnapshot,\n meshSubscribeSse\n} from \"./presenceRoomMesh.js\";\nimport { runRhizohBrainV2 } from \"./rhizohBrainV2.js\";\nimport {\n listConnections,\n createConnection,\n updateConnection,\n deleteConnection,\n setDefaultConnection,\n resolveConnection\n} from \"./llmConnectionsStore.js\";\nimport { checkHttpRateLimit, getHttpClientIp } from \"./castleHttpRateLimit.js\";\nimport {\n submitAbuseReportV0,\n listModerationQueueV0,\n softBlockUserV0,\n removeSoftBlockV0,\n listSoftBlockedUsersV0\n} from \"./ops/moderationMvpV0.js\";\nimport { listRecentAgentSnapshotsV0 } from \"./ops/agentObservabilityV0.js\";\nimport { readAgentContainmentConfigV0 } from \"./ops/agentContainmentV0.js\";\nimport { readCostContainmentConfigV0, getCostContainmentStatsV0 } from \"./ops/costContainmentV0.js\";\nimport { getGlobalCostLedgerSnapshotV0, getGclBackendStatusV0 } from \"./ops/globalCostLedgerV0.js\";\nimport { evaluateEconomicStrategyL3V0 } from \"./ops/economicStrategyEngineL3V0.js\";\nimport { getPhasedRolloutStatsV0, readPhasedRolloutTierV0 } from \"./ops/phasedRolloutV0.js\";\nimport { buildUnifiedStateNarrativeV0 } from \"./ops/unifiedStateNarrativeV0.js\";\nimport { resolveHardeningTenantIdV0 } from \"./ops/narrativeTenantIsolationV0.js\";\nimport {\n classifyStressResponseV0,\n verifyStressTaxonomyCoverageV0,\n verifyStressConflictResolutionV0\n} from \"./ops/stressResponseTaxonomyV0.js\";\nimport { verifyResolutionStabilityEnvelopeV0 } from \"./ops/resolutionStabilityEnvelopeV0.js\";\nimport { logLlmAccess } from \"./castleLlmAudit.js\";\nimport { appendMemory, listMemories, getMemoryContext, getPersonaGoalMemory, setPersonaGoalMemory, autoCompactMemories } from \"./memoryStore.js\";\nimport { getFirebasePersistence } from \"./firebasePersistence.js\";\nimport { registerAgentIdentity, listAgentIdentities, getAgentIdentity, updateAgentIdentity } from \"./agentIdentityStore.js\";\nimport { queueAcademyEvent, listAcademyEvents, resolveAcademyEvent, runAcademyEventTick } from \"./academyEventEngine.js\";\nimport { enqueuePdfJob, getPdfJob } from \"./pdfPipeline.js\";\nimport {\n createPublishSession,\n listPublishSessions,\n updatePublishSession,\n addTranscript,\n listTranscripts,\n searchMetadata,\n registerDevice,\n listDevices,\n updateDevice,\n appendTelemetry,\n listTelemetry,\n createAutomation,\n listAutomations,\n tickAutomations,\n enqueueCommand,\n listCommands,\n updateCommandStatus,\n registerSocialChannel,\n listSocialChannels\n} from \"./studioOpsStore.js\";\nimport { metrics as infraMetrics } from \"./infra/metrics.js\";\nimport { renderRhizohPrometheusMetrics, rhizohEnterpriseMetrics } from \"./infra/rhizohEnterpriseMetrics.js\";\nimport { scoreHealth } from \"./infra/healthScore.js\";\nimport { getTraceById, listTracesBySession } from \"./infra/traceRegistry.js\";\nimport { canonicalEpistemicSealString, hashAndSignEpistemicSeal, EPISTEMIC_SEAL_SCHEMA } from \"./epistemicSeal.js\";\nimport { persistEpistemicLedgerBatch } from \"./epistemicLedgerStore.js\";\nimport { persistEpistemicForecastBatch } from \"./epistemicForecastStore.js\";\nimport { buildRhizohExternalGroundTruthPayload } from \"./rhizohExternalGroundTruthGateway.js\";\nimport { ingestRhizohExternalLossBatchHttp } from \"./rhizohExternalLossIngestGateway.js\";\nimport {\n ingestRhizohProductOutcomeHttp,\n listRhizohProductOutcomeAggregates,\n verifyRhizohOutcomeHmac,\n verifyRhizohOutcomeSourceToken\n} from \"./rhizohProductOutcomeIngestGateway.js\";\nimport { initRhizoh } from \"./rhizohProductionBootstrap.js\";\nimport {\n buildGenesisRuntimeSurfacePayload,\n recordGenesisEpistemicLedgerPersisted,\n recordGenesisEpistemicSealIssued,\n startGenesisCanonicalClock\n} from \"./genesisRuntimeSurfaceV0.js\";\nimport { registerGenesisSseClient, getGenesisContinuitySeq, setGenesisContinuityAfterPublishHook } from \"./genesisContinuityStreamHubV0.js\";\nimport { computeGenesisReplayFingerprintV0 } from \"./genesisReplayFingerprintV0.js\";\nimport { buildGenesisGatewayCapabilitiesV0 } from \"./genesisGatewayCapabilitiesV0.js\";\nimport {\n installGenesisCheckpointSurfaceGetter,\n noteGenesisCheckpointSeqCommitted,\n getLatestGenesisSignedCheckpoint\n} from \"./genesisContinuityCheckpointV0.js\";\nimport { hydrateGenesisContinuityPersistenceBootV0 } from \"./genesisContinuityHydrateBootV0.js\";\nimport { startGenesisContinuityInfraSampler } from \"./genesisContinuityInfraSamplerV0.js\";\nimport {\n genesisCheckpointQueryBySeqV0,\n genesisCheckpointQueryRangeV0,\n genesisCheckpointQueryLineageV0\n} from \"./genesisCheckpointQueryV0.js\";\nimport {\n genesisContinuityEventArchiveEnabled,\n queryGenesisContinuityEventArchiveV0,\n GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION\n} from \"./genesisContinuityEventArchiveV0.js\";\nimport { resolveGenesisReplayRouterV1, GENESIS_REPLAY_ROUTER_SCHEMA } from \"./genesisReplayRouterV1.js\";\nimport {\n computeGenesisReplayTemporalDiffV1,\n GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA\n} from \"./genesisReplayTemporalDiffV1.js\";\nimport { compareGenesisReplayEquivalenceV1 } from \"./genesisReplayEquivalenceV1.js\";\nimport { computeGenesisReplayAnalyticsV1 } from \"./genesisReplayAnalyticsV1.js\";\nimport { computeGenesisReplayEvolutionV1 } from \"./genesisReplayEvolutionV1.js\";\nimport { handleStripeCheckoutCreate, handleStripeMembershipWebhook } from \"./stripeWebhookMembership.js\";\nimport { buildRealityHealthPayload } from \"./edgeRealityConsistencyV1.js\";\nimport {\n assertWalPeerSocketAuthorityV0,\n buildGatewaySubstrateAuthoritySnapshotV0,\n logGatewaySubstrateAuthorityGuardsV0,\n validateWalPeerFeedSignatureV0\n} from \"./gatewaySubstrateAuthorityV0.js\";\nimport {\n buildSubstrateOperationalSnapshotV0,\n recordWalPeerRejectV0,\n renderSubstratePrometheusMetricsV0\n} from \"./infra/substrateOperationalMetrics.js\";\nimport { ingestClientSubstrateHealthV0 } from \"./rhizohSubstrateHealthIngestV0.js\";\nimport { recordSubstrateOtelEventV0 } from \"./infra/opentelemetrySubstrateV0.js\";\nimport {\n GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n getGenesisWorldObservationIngressStatusV0,\n ingestGenesisWorldObservationV0\n} from \"./genesisContinuityClientIngressV0.js\";\nimport {\n getGenesisContinuitySeqAuditV0,\n noteGenesisContinuitySeqAcceptedV0\n} from \"./genesisContinuitySeqGapV0.js\";\n\n/** Render / Fly / Railway: `PORT` — yoksa `CASTLE_GATEWAY_PORT` — yoksa 8090. */\nconst PORT =\n Number(process.env.PORT) ||\n Number(process.env.CASTLE_GATEWAY_PORT) ||\n 8090;\n/** Spiral / world WebSocket server — set after `wss` init; used by genesis runtime surface. */\nlet spiralWssForGenesis = null;\nconst MAX_MESSAGE_BYTES = Number(process.env.CASTLE_MAX_MESSAGE_BYTES || 32 * 1024);\nconst REQUIRED_GATEWAY_TOKEN = process.env.CASTLE_GATEWAY_TOKEN || \"\";\nconst ALLOWED_ORIGINS = (process.env.CASTLE_ALLOWED_ORIGINS || \"\")\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n\nfunction normalizeHttpOrigin(origin) {\n return String(origin || \"\")\n .trim()\n .replace(/\\/+$/, \"\");\n}\n\n/** HTTP CORS: `CASTLE_ALLOWED_ORIGINS` + `CASTLE_HTTP_CORS_ORIGIN` (tek origin); `CASTLE_HTTP_CORS_ORIGIN=*` → wildcard. */\nfunction buildHttpCorsOriginAllowSet() {\n const set = new Set();\n for (const o of ALLOWED_ORIGINS) {\n const n = normalizeHttpOrigin(o);\n if (n) set.add(n);\n }\n const primary = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n if (primary && primary !== \"*\") set.add(primary);\n return set;\n}\n\nconst HTTP_CORS_ORIGIN_ALLOW_SET = buildHttpCorsOriginAllowSet();\n\n/**\n * Tarayıcı `Origin` ile tam eşleşme ister. Tek sabit `CASTLE_HTTP_CORS_ORIGIN` yeterli değil:\n * örn. `web.app` vs `firebaseapp.com` veya Render’da yanlış/eksik env.\n * @returns {string|null} `Access-Control-Allow-Origin` değeri; null = başlık yok (istemci CORS fail).\n */\nfunction accessControlAllowOriginValue(req) {\n const primaryEnv = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n if (primaryEnv === \"*\") return \"*\";\n const reqOrigin = normalizeHttpOrigin(req.headers?.origin);\n if (HTTP_CORS_ORIGIN_ALLOW_SET.size === 0) return \"*\";\n if (!reqOrigin) return \"*\";\n if (HTTP_CORS_ORIGIN_ALLOW_SET.has(reqOrigin)) return reqOrigin;\n return null;\n}\n\nfunction applyHttpCorsHeaders(req, res) {\n const allow = accessControlAllowOriginValue(req);\n if (allow) {\n res.setHeader(\"Access-Control-Allow-Origin\", allow);\n if (allow !== \"*\") res.setHeader(\"Vary\", \"Origin\");\n }\n res.setHeader(\"Access-Control-Allow-Methods\", \"GET,POST,OPTIONS\");\n res.setHeader(\n \"Access-Control-Allow-Headers\",\n \"Content-Type, Authorization, X-Castle-Dev-Uid, X-Castle-Guest-Id, X-Castle-Gateway-Token, X-Castle-Ingress-Contract, X-Rhizoh-Outcome-Signature, X-Rhizoh-Outcome-Source-Token\"\n );\n}\nconst REQUIRE_AUTH = process.env.CASTLE_REQUIRE_AUTH === \"true\";\nconst ALLOW_DEV_ANON = process.env.CASTLE_ALLOW_DEV_ANON !== \"false\";\nconst ALLOW_DEV_HTTP_UID = process.env.CASTLE_ALLOW_DEV_HTTP_UID !== \"false\";\n/** Presence mesh: allow `X-Castle-Guest-Id` when Firebase auth absent (default on). Set `false` to require auth only. */\nconst ALLOW_MESH_GUEST = process.env.CASTLE_ALLOW_MESH_GUEST !== \"false\";\nconst SOCIAL_RETRY_MAX = Math.max(0, Math.min(5, Number(process.env.CASTLE_SOCIAL_RETRY_MAX || 2)));\nconst SOCIAL_RETRY_BASE_MS = Math.max(100, Number(process.env.CASTLE_SOCIAL_RETRY_BASE_MS || 700));\nconst TELEGRAM_BOT_TOKEN = process.env.CASTLE_TELEGRAM_BOT_TOKEN || \"\";\nconst WHATSAPP_TOKEN = process.env.CASTLE_WHATSAPP_TOKEN || \"\";\nconst WHATSAPP_PHONE_NUMBER_ID = process.env.CASTLE_WHATSAPP_PHONE_NUMBER_ID || \"\";\nconst RL_RHIZOH_LLM_PER_MIN = Math.max(5, Number(process.env.CASTLE_RL_RHIZOH_LLM_PER_MIN || 40));\nconst RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN || 30)\n);\nconst RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN || 120));\nconst RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN = Math.max(8, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN || 48));\nconst RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN = Math.max(4, Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN || 24));\nconst RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN || 18)\n);\nconst RL_EPISTEMIC_SEAL_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_EPISTEMIC_SEAL_PER_MIN || 120));\nconst RL_EPISTEMIC_LOG_PER_MIN = Math.max(20, Number(process.env.CASTLE_RL_EPISTEMIC_LOG_PER_MIN || 240));\nconst EPISTEMIC_SEAL_SECRET = String(\n process.env.CASTLE_EPISTEMIC_SEAL_SECRET || process.env.CASTLE_GATEWAY_TOKEN || \"\"\n).trim();\nconst RL_LLM_CONN_TEST_PER_MIN = Math.max(2, Number(process.env.CASTLE_RL_LLM_CONNECTION_TEST_PER_MIN || 8));\n\n/** One-shot production warnings: CORS surface, auth toggle, dev bypass flags (observatory deploy hygiene). */\nfunction logProductionObservatorySurfaceGuardsV0() {\n if (String(process.env.NODE_ENV || \"\").toLowerCase() !== \"production\") return;\n const tag = \"[GATEWAY_PROD_GUARD]\";\n const primaryCors = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n const corsListEmpty = ALLOWED_ORIGINS.length === 0;\n if (corsListEmpty && !primaryCors) {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty and CASTLE_HTTP_CORS_ORIGIN empty — browser SSE/replay may fail; set explicit Firebase Hosting origins`\n );\n } else if (corsListEmpty && primaryCors && primaryCors !== \"*\") {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty — only CASTLE_HTTP_CORS_ORIGIN set; add web.app + firebaseapp.com (and previews) to CASTLE_ALLOWED_ORIGINS if clients use alternate hostnames`\n );\n }\n if (primaryCors && /localhost|127\\.0\\.0\\.1/i.test(primaryCors)) {\n console.warn(`${tag} CORS: CASTLE_HTTP_CORS_ORIGIN is localhost while NODE_ENV=production`);\n }\n if (REQUIRE_AUTH) {\n console.warn(\n `${tag} AUTH: CASTLE_REQUIRE_AUTH=true — clients must send a valid gateway credential (otherwise silent 401 on replay/SSE); EventSource cannot set custom headers unless proxied`\n );\n if (REQUIRED_GATEWAY_TOKEN.length < 16) {\n console.warn(`${tag} AUTH: CASTLE_GATEWAY_TOKEN missing or short while require-auth enabled`);\n }\n }\n if (ALLOW_DEV_ANON) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_ANON is not \"false\" — dev anonymous path may be live in production (set CASTLE_ALLOW_DEV_ANON=false)`\n );\n }\n if (ALLOW_DEV_HTTP_UID) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_HTTP_UID is not \"false\" — X-Castle-Dev-Uid bypass risk (set CASTLE_ALLOW_DEV_HTTP_UID=false)`\n );\n }\n}\n\nconst rhizohRuntime = initRhizoh();\n\n/** İstemci: llmKeySource veya keySource — env | user_connection | auto */\nfunction normalizeClientLlmKeySource(raw) {\n const s = String(raw ?? \"\").trim().toLowerCase();\n if (s === \"env\" || s === \"server\") return \"env\";\n if (s === \"user_connection\" || s === \"connection\" || s === \"user\") return \"user_connection\";\n if (s === \"auto\" || s === \"\") return \"auto\";\n return null;\n}\n\nfunction sendJson(res, statusCode, payload) {\n res.writeHead(statusCode, { \"Content-Type\": \"application/json; charset=utf-8\" });\n res.end(JSON.stringify(payload));\n}\n\nfunction logRhizohHealth(stage, detail = {}) {\n try {\n console.info(`[CASTLE_${String(stage || \"unknown\")}]`, detail && typeof detail === \"object\" ? detail : {});\n } catch {\n /* noop */\n }\n}\n\nfunction renderInfraMetricsProm() {\n const lines = [\n `castle_gateway_events_processed_total ${infraMetrics.eventsProcessed}`,\n `castle_gateway_duplicate_reject_total ${infraMetrics.duplicateRejects}`,\n `castle_gateway_enqueue_latency_ms ${infraMetrics.enqueueLatencyMs}`,\n `castle_gateway_queue_depth ${infraMetrics.queueDepth}`,\n `castle_gateway_queue_lag_seconds ${(Number(infraMetrics.queueLag || 0) / 1000).toFixed(3)}`,\n `castle_gateway_errors_total ${infraMetrics.errors}`\n ];\n const buckets = infraMetrics.enqueueLatencyBuckets || {};\n for (const k of Object.keys(buckets)) {\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"${k}\"} ${buckets[k]}`);\n }\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"+Inf\"} ${infraMetrics.eventsProcessed}`);\n if (process.env.CASTLE_SUBSTRATE_METRICS !== \"0\") {\n lines.push(renderSubstratePrometheusMetricsV0().trimEnd());\n }\n return lines.join(\"\\n\");\n}\n\nasync function fetchJsonWithTimeout(url, timeoutMs = 1200) {\n const ctl = new AbortController();\n const tid = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const res = await fetch(url, { signal: ctl.signal });\n if (!res.ok) return null;\n return await res.json();\n } catch {\n return null;\n } finally {\n clearTimeout(tid);\n }\n}\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction getHttpPathname(req) {\n let p = String(req.url || \"\").split(\"?\")[0];\n /* Reverse proxies / clients sometimes append `/` — avoid silent 404 on e.g. `/rhizoh/genesis/stream/`. */\n while (p.length > 1 && p.endsWith(\"/\")) {\n p = p.slice(0, -1);\n }\n return p;\n}\n\nfunction rhizohLlmEnvConfigured() {\n const p = String(process.env.CASTLE_LLM_PROVIDER || \"openai\").toLowerCase();\n const key =\n p === \"anthropic\"\n ? process.env.ANTHROPIC_API_KEY\n : p === \"gemini\"\n ? process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY\n : p === \"xai\"\n ? process.env.XAI_API_KEY\n : p === \"deepseek\"\n ? process.env.DEEPSEEK_API_KEY\n : p === \"mistral\"\n ? process.env.MISTRAL_API_KEY\n : p === \"openrouter\"\n ? process.env.OPENROUTER_API_KEY\n : process.env.OPENAI_API_KEY;\n return !!String(key || \"\").trim();\n}\n\n/**\n * @param {import(\"firebase-admin/firestore\").Firestore | null} db\n * @param {number} timeoutMs\n */\nasync function probeFirestoreReachable(db, timeoutMs = 2000) {\n if (!db) return { ok: false, reason: \"no_db\" };\n const timeout = sleep(timeoutMs).then(() => ({ ok: false, reason: \"timeout\" }));\n const work = db\n .collection(\"_castle_gateway_health\")\n .doc(\"ping\")\n .get()\n .then(() => ({ ok: true, reason: \"reachable\" }))\n .catch((e) => ({ ok: false, reason: String(e?.message || e || \"firestore_error\") }));\n return Promise.race([work, timeout]);\n}\n\nfunction stripHtml(text = \"\") {\n return String(text)\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \" \")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \" \")\n .replace(/<[^>]+>/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction haversineMeters(aLat, aLon, bLat, bLon) {\n const R = 6371000;\n const dLat = ((bLat - aLat) * Math.PI) / 180;\n const dLon = ((bLon - aLon) * Math.PI) / 180;\n const sa = Math.sin(dLat / 2);\n const sb = Math.sin(dLon / 2);\n const x = sa * sa + Math.cos((aLat * Math.PI) / 180) * Math.cos((bLat * Math.PI) / 180) * sb * sb;\n return 2 * R * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));\n}\n\nasync function checkPublishTargetHealth(protocol, target) {\n const p = String(protocol || \"\").toUpperCase();\n const t = String(target || \"\").trim();\n if (!t) return { ok: false, reason: \"target_missing\", shouldReconnect: false };\n if (p === \"RTMP\") {\n const ok = t.startsWith(\"rtmp://\") || t.startsWith(\"rtmps://\");\n return { ok, reason: ok ? \"rtmp_target_format_ok\" : \"invalid_rtmp_target\", shouldReconnect: !ok, reconnectInSec: 15 };\n }\n if (p === \"WHIP\" || p === \"SFU\") {\n try {\n const res = await fetch(t, { method: \"OPTIONS\" });\n const ok = res.status < 500;\n return { ok, reason: `http_${res.status}`, shouldReconnect: !ok, reconnectInSec: 8 };\n } catch {\n return { ok: false, reason: \"network_unreachable\", shouldReconnect: true, reconnectInSec: 8 };\n }\n }\n return { ok: true, reason: \"unchecked_protocol\", shouldReconnect: false };\n}\n\nasync function fetchTextFromUrl(url) {\n const res = await fetch(url, { method: \"GET\" });\n if (!res.ok) throw new Error(`fetch_${res.status}`);\n const ct = String(res.headers.get(\"content-type\") || \"\");\n if (ct.includes(\"application/pdf\")) throw new Error(\"pdf_binary_not_supported_directly\");\n const body = await res.text();\n return stripHtml(body).slice(0, 14000);\n}\n\nfunction buildTaskActionGraph(task = \"\", context = {}) {\n const t = String(task || \"\").toLowerCase();\n if (t.includes(\"dinner\") || t.includes(\"akşam yemeği\") || t.includes(\"aksam yemegi\")) {\n return {\n taskTitle: \"Dinner Prep Planner\",\n nodes: [\n { id: \"n1\", title: \"Kitchen preheat\", action: \"smart-oven.preheat\", params: { temperatureC: 190, mode: \"fan\" } },\n { id: \"n2\", title: \"Lighting comfort\", action: \"smart-light.set_scene\", params: { scene: \"warm_dinner\" } },\n { id: \"n3\", title: \"Fridge inventory check\", action: \"smart-fridge.inventory_scan\", params: {} },\n { id: \"n4\", title: \"Reminder notification\", action: \"smart-watch.notify\", params: { text: \"Dinner prep started.\" } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" },\n { from: \"n3\", to: \"n4\" }\n ]\n };\n }\n return {\n taskTitle: task || \"General Planner\",\n nodes: [\n { id: \"n1\", title: \"Safety check\", action: \"device.safety_check\", params: { context } },\n { id: \"n2\", title: \"Primary action\", action: \"device.execute_task\", params: { task } },\n { id: \"n3\", title: \"Report\", action: \"studio.timeline_note\", params: { task } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" }\n ]\n };\n}\n\nfunction buildEthicsPrograms() {\n return {\n policyVersion: \"v1\",\n tracks: [\n {\n id: \"elderly-care-assist\",\n audience: \"elderly\",\n title: \"Elderly Assistance Track\",\n principles: [\"human-in-the-loop\", \"consent-first\", \"low-friction alerts\"],\n modules: [\"medication-reminder\", \"fall-risk-check\", \"family-notify\", \"daily-companion-brief\"]\n },\n {\n id: \"kids-safe-learning\",\n audience: \"children\",\n title: \"Kids Safe Learning Track\",\n principles: [\"guardian-consent\", \"age-appropriate-language\", \"content-filtering\"],\n modules: [\"study-coach\", \"screen-time-balance\", \"safe-social-share\", \"emergency-contact\"]\n }\n ]\n };\n}\n\nasync function deliverViaTelegram(channel, text) {\n const chatId = String(channel.endpoint || \"\").trim();\n if (!TELEGRAM_BOT_TOKEN) throw new Error(\"telegram_token_missing\");\n if (!chatId) throw new Error(\"telegram_chat_id_missing\");\n const url = `https://api.telegram.org/bot${encodeURIComponent(TELEGRAM_BOT_TOKEN)}/sendMessage`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ chat_id: chatId, text })\n });\n if (!res.ok) throw new Error(`telegram_http_${res.status}`);\n return { provider: \"telegram\", status: res.status };\n}\n\nasync function deliverViaWhatsapp(channel, text) {\n const to = String(channel.endpoint || \"\").trim();\n if (!WHATSAPP_TOKEN) throw new Error(\"whatsapp_token_missing\");\n if (!WHATSAPP_PHONE_NUMBER_ID) throw new Error(\"whatsapp_phone_number_id_missing\");\n if (!to) throw new Error(\"whatsapp_to_missing\");\n const url = `https://graph.facebook.com/v22.0/${encodeURIComponent(WHATSAPP_PHONE_NUMBER_ID)}/messages`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${WHATSAPP_TOKEN}` },\n body: JSON.stringify({ messaging_product: \"whatsapp\", to, type: \"text\", text: { body: text } })\n });\n if (!res.ok) throw new Error(`whatsapp_http_${res.status}`);\n return { provider: \"whatsapp\", status: res.status };\n}\n\nasync function deliverViaWebhook(channel, text) {\n const endpoint = String(channel.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"webhook_endpoint_missing\");\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text, platform: channel.platform || \"webhook\", ts: Date.now() })\n });\n if (!res.ok) throw new Error(`webhook_http_${res.status}`);\n return { provider: \"webhook\", status: res.status };\n}\n\nasync function deliverSocialMessage(channel, text) {\n const platform = String(channel.platform || \"webhook\").toLowerCase();\n if (platform === \"telegram\") return deliverViaTelegram(channel, text);\n if (platform === \"whatsapp\") return deliverViaWhatsapp(channel, text);\n return deliverViaWebhook(channel, text);\n}\n\nasync function deliverSocialWithRetry(channel, text) {\n let attempt = 0;\n let lastError = null;\n while (attempt <= SOCIAL_RETRY_MAX) {\n try {\n const result = await deliverSocialMessage(channel, text);\n return { ok: true, attempt: attempt + 1, ...result };\n } catch (error) {\n lastError = error;\n if (attempt >= SOCIAL_RETRY_MAX) break;\n const jitter = Math.floor(Math.random() * 90);\n await sleep(SOCIAL_RETRY_BASE_MS * Math.pow(2, attempt) + jitter);\n }\n attempt += 1;\n }\n return { ok: false, attempt: attempt + 1, error: String(lastError?.message || \"delivery_failed\") };\n}\n\nasync function dispatchCommandToAdapter(device, command) {\n const adapter = String(command?.adapter || device?.adapter || \"websocket\").toLowerCase();\n const endpoint = String(device?.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"device_endpoint_missing\");\n if (adapter === \"mqtt\") {\n // MQTT delivery is proxied via a bridge endpoint (HTTP webhook style).\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n type: \"mqtt-command\",\n topic: `castle/devices/${device.id}/commands`,\n action: command.action,\n params: command.params || {}\n })\n });\n if (!res.ok) throw new Error(`mqtt_bridge_http_${res.status}`);\n return { ok: true, adapter, via: \"http-bridge\", status: res.status };\n }\n if (!endpoint.startsWith(\"ws://\") && !endpoint.startsWith(\"wss://\")) throw new Error(\"invalid_websocket_endpoint\");\n const sendResult = await new Promise((resolve, reject) => {\n const ws = new WebSocket(endpoint);\n const timer = setTimeout(() => {\n try {\n ws.close();\n } catch {\n /* noop */\n }\n reject(new Error(\"websocket_timeout\"));\n }, 3000);\n ws.on(\"open\", () => {\n ws.send(\n JSON.stringify({\n type: \"castle-command\",\n commandId: command.id,\n deviceId: command.deviceId,\n action: command.action,\n params: command.params || {}\n }),\n (err) => {\n clearTimeout(timer);\n ws.close();\n if (err) reject(new Error(\"websocket_send_failed\"));\n else resolve({ ok: true, adapter: \"websocket\", via: \"ws\" });\n }\n );\n });\n ws.on(\"error\", () => {\n clearTimeout(timer);\n reject(new Error(\"websocket_connect_failed\"));\n });\n });\n return sendResult;\n}\n\nfunction readHttpJson(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => {\n if (!buf) return resolve({});\n const payload = safeJsonParse(buf);\n if (!payload || typeof payload !== \"object\") return reject(new Error(\"invalid_json_payload\"));\n resolve(payload);\n });\n req.on(\"error\", reject);\n });\n}\n\n/** Ham gövde (HMAC doğrulaması için). */\nfunction readHttpBodyText(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => resolve(buf));\n req.on(\"error\", reject);\n });\n}\n\nfunction maskOpaqueId(value) {\n const t = String(value || \"\");\n if (!t) return \"(empty)\";\n if (t.length <= 8) return \"****\";\n return `${t.slice(0, 4)}…${t.slice(-2)}`;\n}\n\nfunction readGatewayHttpToken(req) {\n const x = String(req.headers?.[\"x-castle-gateway-token\"] || \"\").trim();\n if (x) return x;\n const auth = String(req.headers?.authorization || \"\");\n const m = /^Bearer\\s+(.+)$/i.exec(auth);\n return m ? String(m[1] || \"\").trim() : \"\";\n}\n\nasync function resolveHttpUser(req) {\n const authResult = await verifyClientToken(req);\n if (authResult?.ok && authResult.user?.uid) {\n return { ok: true, uid: String(authResult.user.uid), mode: authResult.kind || \"auth\" };\n }\n if (ALLOW_DEV_ANON && ALLOW_DEV_HTTP_UID) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) return { ok: true, uid: `dev-${devUid.slice(0, 64)}`, mode: \"dev-header\" };\n }\n return { ok: false, reason: authResult?.reason || \"auth_required\" };\n}\n\n/** Mesh actor: Firebase uid, dev header, or `guest:<uuid>` via header / `guestId` query (SSE cannot set headers). */\nasync function resolveMeshActor(req) {\n const auth = await resolveHttpUser(req);\n if (auth.ok) return { ok: true, clientUid: auth.uid, mode: auth.mode };\n if (ALLOW_MESH_GUEST) {\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const gid = String(req.headers?.[\"x-castle-guest-id\"] || q.searchParams.get(\"guestId\") || \"\").trim();\n if (gid) {\n const normalized = gid.startsWith(\"guest:\") ? gid.slice(0, 180) : `guest:${gid.slice(0, 160)}`;\n return { ok: true, clientUid: normalized, mode: \"guest-header\" };\n }\n }\n return { ok: false, reason: \"mesh_auth_required\" };\n}\n\n/** Live genesis runtime JSON (GET /rhizoh/genesis/runtime + SSE infra sampler). */\nasync function buildGenesisRuntimeSurfacePayloadLive() {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n let spiralWsActive = 0;\n if (spiralWssForGenesis) {\n for (const c of spiralWssForGenesis.clients) {\n if (c.readyState === 1) spiralWsActive += 1;\n }\n }\n const base = buildGenesisRuntimeSurfacePayload({\n infraMetrics,\n scoredHealth: scored,\n rhizohEnterpriseMetrics,\n mesh: meshContinuityAggregate(),\n workerHealth: worker,\n port: PORT,\n spiralWebSocketClientsActive: spiralWsActive\n });\n return {\n ...base,\n replayFingerprint: computeGenesisReplayFingerprintV0(base),\n gatewayCapabilities: buildGenesisGatewayCapabilitiesV0(),\n genesisStream: {\n schema: \"castle.genesis.stream_cursor.v0\",\n lastAcceptedSeq: getGenesisContinuitySeq(),\n clientIngress: {\n schema: GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n contract: \"castle.world_observation.ingress_envelope.v1\",\n postPath: rhizohRuntime.routes.genesisIngress,\n closure: getGenesisWorldObservationIngressStatusV0()\n },\n seqContinuity: getGenesisContinuitySeqAuditV0(),\n eventArchive: genesisContinuityEventArchiveEnabled()\n ? {\n enabled: true,\n projection: GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION,\n queryPath: rhizohRuntime.routes.genesisContinuityEvents\n }\n : { enabled: false },\n replayRouter: {\n schema: GENESIS_REPLAY_ROUTER_SCHEMA,\n queryPath: rhizohRuntime.routes.genesisReplay,\n queryHint: \"from=&to= or range=from-to; optional type=&checkpoints=0\",\n temporalDiffPath: rhizohRuntime.routes.genesisReplayDiff,\n temporalDiffSchema: GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA,\n equivalencePath: rhizohRuntime.routes.genesisReplayEquivalence,\n analyticsPath: rhizohRuntime.routes.genesisReplayAnalytics,\n evolutionPath: rhizohRuntime.routes.genesisReplayEvolution\n }\n }\n };\n}\n\nconst httpServer = createServer(async (req, res) => {\n applyHttpCorsHeaders(req, res);\n if (req.method === \"OPTIONS\") {\n res.writeHead(204);\n res.end();\n return;\n }\n\n const pathname = getHttpPathname(req);\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/genesis/__ping\") {\n sendJson(res, 200, {\n ok: true,\n pid: process.pid,\n entry: __filename,\n time: Date.now(),\n genesis: true\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/webhooks/stripe\") {\n await handleStripeMembershipWebhook(req, res);\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/billing/stripe/checkout\") {\n await handleStripeCheckoutCreate(req, res, { readHttpJson, resolveHttpUser, sendJson });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/live\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, live: false, dns: true, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n sendJson(res, 200, { ok: true, live: true, dns: true, service: \"castle-gateway\", ts: Date.now() });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/ready\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, ready: false, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n try {\n const persistence = getFirebasePersistence();\n const llmProbe = rhizohLlmEnvConfigured();\n sendJson(res, 200, {\n ok: true,\n ready: true,\n dns: true,\n persistence: persistence.mode,\n service: \"castle-gateway\",\n gateway: \"ok\",\n llm: llmProbe ? \"ok\" : \"degraded\",\n spine: llmProbe ? \"ok\" : \"partial\"\n });\n } catch (e) {\n sendJson(res, 503, { ok: false, ready: false, reason: String(e?.message || e), service: \"castle-gateway\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/reality\") {\n const payload = buildRealityHealthPayload();\n sendJson(res, payload.ok ? 200 : 503, payload);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/substrate\") {\n const authority = buildGatewaySubstrateAuthoritySnapshotV0();\n const operational = buildSubstrateOperationalSnapshotV0();\n const drift = buildRealityHealthPayload();\n const ok = Boolean(authority.productionAuthoritySatisfied) && drift.ok;\n sendJson(res, ok ? 200 : 503, {\n ok,\n service: \"castle-gateway\",\n ts: Date.now(),\n authority,\n operational,\n drift: { ok: drift.ok, errors: drift.drift?.errors, warnings: drift.drift?.warnings }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/substrate/health\") {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const body = await readHttpJson(req);\n const uid = (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestClientSubstrateHealthV0(body, { uid, clientId: uid });\n if (!result.ok) {\n sendJson(res, 400, result);\n return;\n }\n recordSubstrateOtelEventV0(\"client_health_ingest\", { uid: String(uid).slice(0, 32) });\n sendJson(res, 200, result);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/deps\") {\n const t0 = Date.now();\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, {\n ok: false,\n dns: true,\n llm: false,\n firestore: false,\n phase: \"maintenance\",\n latencyMs: Date.now() - t0,\n service: \"castle-gateway\"\n });\n return;\n }\n const persistence = getFirebasePersistence();\n let firestoreOk = persistence.mode !== \"firebase\";\n let firestoreDetail = persistence.mode === \"file\" ? \"file_mode\" : \"not_applicable\";\n if (persistence.mode === \"firebase\" && persistence.db) {\n const pr = await probeFirestoreReachable(persistence.db, 2000);\n firestoreOk = pr.ok;\n firestoreDetail = pr.reason || (pr.ok ? \"reachable\" : \"fail\");\n } else if (persistence.mode === \"firebase\" && !persistence.db) {\n firestoreOk = false;\n firestoreDetail = \"firebase_not_initialized\";\n }\n const llmOk = rhizohLlmEnvConfigured();\n const overallOk = llmOk && firestoreOk;\n const latencyMs = Date.now() - t0;\n sendJson(res, 200, {\n ok: overallOk,\n dns: true,\n llm: llmOk,\n firestore: persistence.mode === \"firebase\" ? firestoreOk : false,\n persistence: persistence.mode,\n firestoreDetail,\n latencyMs,\n service: \"castle-gateway\",\n wsPort: PORT\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health\") {\n const persistence = getFirebasePersistence();\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n wsPort: PORT,\n persistence: persistence.mode,\n presenceMesh: \"sse\"\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/health\") {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n sendJson(res, 200, {\n ok: true,\n role: \"gateway\",\n queueLagMs: infraMetrics.queueLag,\n queueDepth: infraMetrics.queueDepth,\n errors: infraMetrics.errors,\n status: scored.status,\n score: scored.score,\n reasons: scored.reasons,\n worker: worker\n ? {\n status: worker.status,\n score: worker.score,\n reasons: worker.reasons\n }\n : null,\n timestamp: Date.now()\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisRuntime) {\n const payload = await buildGenesisRuntimeSurfacePayloadLive();\n sendJson(res, 200, payload);\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.genesisIngress) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const actor = await resolveMeshActor(req);\n const clientId = actor.ok ? actor.clientUid : (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestGenesisWorldObservationV0(body, { clientId });\n if (!result.ok) {\n const status =\n result.error === \"rate_limited\" || result.error === \"agent_spoke_throttled\"\n ? 429\n : result.error === \"payload_too_large\"\n ? 413\n : 400;\n sendJson(res, status, result);\n return;\n }\n sendJson(res, result.idempotent ? 200 : 202, result);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisStream) {\n const corsOrigin = accessControlAllowOriginValue(req);\n if (!corsOrigin && String(process.env.NODE_ENV || \"\").toLowerCase() === \"production\") {\n const og = String(req.headers?.origin || \"\").trim();\n if (og) {\n console.warn(\"[GATEWAY] genesis SSE: missing Access-Control-Allow-Origin for browser Origin (CORS whitelist mismatch):\", og);\n }\n }\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n res.write(\"retry: 3000\\n\\n\");\n res.write(\": genesis continuity stream\\n\\n\");\n res.write(\"event: genesis\\n\");\n res.write(`data: {\"booted\":true,\"pid\":${process.pid}}\\n\\n`);\n registerGenesisSseClient(res, req);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLatest) {\n const cp = getLatestGenesisSignedCheckpoint();\n if (!cp) {\n sendJson(res, 404, { ok: false, error: \"no_checkpoint_yet\", hint: \"wait_for_seq_interval_or_configure_signing_secret\" });\n return;\n }\n sendJson(res, 200, { ok: true, checkpoint: cp });\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(rhizohRuntime.routes.genesisCheckpointBySeqPrefix)) {\n const tail = pathname.slice(rhizohRuntime.routes.genesisCheckpointBySeqPrefix.length).split(\"/\")[0];\n const seq = parseInt(String(tail || \"\").trim(), 10);\n const out = await genesisCheckpointQueryBySeqV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"checkpoint_not_found\") {\n sendJson(res, 404, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointRange) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryRangeV0(from, to);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"range_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLineage) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const seq = parseInt(String(u.searchParams.get(\"seq\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryLineageV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"lineage_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisContinuityEvents) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const limit = parseInt(String(u.searchParams.get(\"limit\") || \"\").trim(), 10);\n const out = await queryGenesisContinuityEventArchiveV0(from, to, type, limit);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\" || out.error === \"event_archive_disabled\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"archive_file_too_large\") {\n sendJson(res, 413, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplay) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await resolveGenesisReplayRouterV1({\n from: fromSeq,\n to: toSeq,\n type,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayDiff) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const out = await computeGenesisReplayTemporalDiffV1({ from: fromSeq, to: toSeq, type });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEquivalence) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from1 = parseInt(String(u.searchParams.get(\"from1\") || \"\").trim(), 10);\n const to1 = parseInt(String(u.searchParams.get(\"to1\") || \"\").trim(), 10);\n const from2 = parseInt(String(u.searchParams.get(\"from2\") || \"\").trim(), 10);\n const to2 = parseInt(String(u.searchParams.get(\"to2\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await compareGenesisReplayEquivalenceV1({\n from1,\n to1,\n from2,\n to2,\n type,\n includeCheckpoints\n });\n if (\n !out.ok &&\n (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")\n ) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayAnalytics) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayAnalyticsV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEvolution) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const collapseWindows = parseInt(String(u.searchParams.get(\"collapseWindows\") || \"0\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayEvolutionV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n collapseWindows,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/metrics\") {\n res.writeHead(200, { \"Content-Type\": \"text/plain; version=0.0.4; charset=utf-8\" });\n res.end(renderInfraMetricsProm());\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.infraPrometheusMetrics) {\n if (process.env.CASTLE_PROMETHEUS_METRICS !== \"1\") {\n sendJson(res, 404, { ok: false, error: \"prometheus_metrics_disabled\" });\n return;\n }\n res.writeHead(200, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(renderRhizohPrometheusMetrics());\n return;\n }\n if (req.method === \"GET\" && pathname.startsWith(\"/infra/traces/\")) {\n const traceId = decodeURIComponent(pathname.slice(\"/infra/traces/\".length));\n const item = await getTraceById(traceId);\n if (!item) return sendJson(res, 404, { ok: false, error: \"trace_not_found\" });\n return sendJson(res, 200, { ok: true, trace: item });\n }\n if (req.method === \"GET\" && pathname === \"/infra/traces\") {\n const u = new URL(String(req.url || \"/infra/traces\"), `http://${req.headers.host || \"localhost\"}`);\n const sessionId = String(u.searchParams.get(\"sessionId\") || \"\").trim();\n const items = sessionId ? await listTracesBySession(sessionId) : [];\n return sendJson(res, 200, { ok: true, sessionId, traces: items });\n }\n\n /** Presence room mesh (C): JOIN / LEAVE / SNAPSHOT / DELTA / REPLAY (HTTP); SUBSCRIBE = SSE stream. */\n if (req.method === \"POST\" && pathname === \"/presence/mesh/join\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid, clientUid: actor.clientUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/leave\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshLeave(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/snapshot\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n sendJson(res, 200, meshSnapshot(roomUid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/delta\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, MAX_MESSAGE_BYTES);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const seq = meshAppendDelta(roomUid, actor.clientUid, {\n node: body?.node,\n projectionPatch: body?.projectionPatch,\n writerSubject: body?.writerSubject\n });\n sendJson(res, 200, { ok: true, roomUid, seq });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/replay\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const fromNodeId = body?.fromNodeId != null ? String(body.fromNodeId) : \"\";\n const fromSeq = body?.fromSeq;\n if (fromNodeId) {\n sendJson(res, 200, meshReplayFromNodeId(roomUid, fromNodeId));\n return;\n }\n sendJson(res, 200, meshReplayFromSeq(roomUid, fromSeq));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/presence/mesh/subscribe\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const roomUid = String(q.searchParams.get(\"roomUid\") || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n const write = (chunk) => {\n try {\n res.write(chunk);\n } catch {\n /* closed */\n }\n };\n const unsub = meshSubscribeSse(roomUid, write);\n const onClose = () => {\n unsub();\n meshLeave(roomUid, actor.clientUid);\n try {\n res.end();\n } catch {\n /* ignore */\n }\n };\n req.on(\"close\", onClose);\n req.on(\"aborted\", onClose);\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, uid: auth.uid, mode: auth.mode, items: await listConnections(auth.uid) });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const id = await createConnection(auth.uid, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"create_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n await updateConnection(auth.uid, id, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"update_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"DELETE\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n await deleteConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"delete_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections/test\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n if (!checkHttpRateLimit(`llm_conn_test:${auth.uid}`, RL_LLM_CONN_TEST_PER_MIN, 60_000)) {\n return sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n }\n const hasBodyKey = !!String(payload?.apiKey || \"\").trim();\n const result = await queryRhizohLlm(\n {\n message: String(payload?.message || \"Ping from Castle Gateway\"),\n context: payload?.context || { source: \"connection_test\" },\n provider: payload?.provider,\n model: payload?.model,\n apiKey: payload?.apiKey || \"\"\n },\n { allowExternalApiKey: true, llmKeySource: hasBodyKey ? \"auto\" : \"env\" }\n );\n logLlmAccess({\n route: \"/llm/connections/test\",\n uid: auth.uid,\n llmKeyBillingOwner: result.llmKeyBillingOwner,\n llmKeyOrigin: result.llmKeyOrigin,\n provider: result.provider,\n model: result.model,\n connectionId: null\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"connection_test_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAgentIdentities(auth.uid);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerAgentIdentity({\n ownerUid: auth.uid,\n agentId: payload?.agentId,\n role: payload?.role,\n personaSeed: payload?.personaSeed || {},\n capabilityLevel: payload?.capabilityLevel\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/agents/identities/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const owns = await getAgentIdentity(id);\n if (!owns || owns.ownerUid !== auth.uid) return sendJson(res, 403, { ok: false, error: \"forbidden\" });\n const payload = await readHttpJson(req);\n const row = await updateAgentIdentity(id, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/academy/events\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/academy/events\"}`);\n const status = String(url.searchParams.get(\"status\") || \"\");\n const items = await listAcademyEvents({ ownerUid: auth.uid, status, limit: 120 });\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/academy/events\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const ev = await queueAcademyEvent({\n type: payload?.type,\n roomId: payload?.roomId,\n ownerUid: auth.uid,\n participants: payload?.participants || [],\n topic: payload?.topic || \"academy\",\n payload: payload?.payload || {}\n });\n sendJson(res, 200, { ok: true, event: ev });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && String(req.url || \"\").startsWith(\"/academy/events/\") && String(req.url || \"\").endsWith(\"/resolve\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const parts = String(req.url || \"\").split(\"/\").filter(Boolean);\n const eventId = parts[2] || \"\";\n try {\n const payload = await readHttpJson(req);\n const out = await resolveAcademyEvent(eventId, payload || {});\n sendJson(res, 200, { ok: true, event: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_resolve_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/memory\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/memory\"}`);\n const scope = url.searchParams.get(\"scope\") === \"agents\" ? \"agents\" : \"users\";\n const id = String(url.searchParams.get(\"id\") || (scope === \"agents\" ? \"\" : auth.uid));\n const kind = String(url.searchParams.get(\"kind\") || \"\");\n const limit = Number(url.searchParams.get(\"limit\") || 40);\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n sendJson(res, 200, { ok: true, scope, id, kind, items: await listMemories({ scope, id, kind, limit }) });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const profile = await getPersonaGoalMemory(auth.uid);\n sendJson(res, 200, { ok: true, profile });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const profile = await setPersonaGoalMemory({\n uid: auth.uid,\n personaPatch: payload?.personaPatch || {},\n goals: Array.isArray(payload?.goals) ? payload.goals : null,\n preferencesPatch: payload?.preferencesPatch || {}\n });\n sendJson(res, 200, { ok: true, profile });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"profile_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/compact\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const out = await autoCompactMemories({ scope, id });\n sendJson(res, 200, { ok: true, scope, id, ...out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_compact_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/context\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const context = await getMemoryContext({\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n query: String(payload?.query || \"\"),\n limit: Number(payload?.limit || 80)\n });\n sendJson(res, 200, { ok: true, context });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_context_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/place-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const placeName = String(payload?.placeName || \"\").trim();\n if (!placeName) return sendJson(res, 400, { ok: false, error: \"placeName_required\" });\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const persona = String(payload?.waypointPersona || payload?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(placeName)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Place: ${placeName}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Give a concise conversational briefing about this place for a walking-route companion.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-place-brief\", placeName, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `PLACE_BRIEF:${placeName} => ${String(summary.reply || \"\").slice(0, 900)}`,\n tags: [\"place\", \"brief\", \"event-layer\"],\n importance: 0.62,\n meta: { placeName }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"place-brief\",\n text: `${placeName} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: {\n placeName,\n directive: summary.directive,\n lat: hit?.lat ? Number(hit.lat) : null,\n lon: hit?.lon ? Number(hit.lon) : null\n }\n });\n sendJson(res, 200, { ok: true, place: hit || null, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"place_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/route-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const waypoints = Array.isArray(payload?.waypoints) ? payload.waypoints.slice(0, 24) : [];\n const out = [];\n for (const wp of waypoints) {\n const name = String(wp?.name || \"\").trim();\n if (!name) continue;\n const persona = String(wp?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(name)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Waypoint: ${name}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Generate a concise spoken briefing for route waypoint.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-route-brief\", waypoint: name, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n out.push({\n name,\n persona,\n place: hit || null,\n reply: summary.reply,\n directive: summary.directive\n });\n }\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `ROUTE_BRIEF: waypoints=${out.length} ${out.map((x) => `${x.name}:${x.persona}`).join(\" | \")}`.slice(0, 1300),\n tags: [\"route\", \"waypoint\", \"brief\", \"event-layer\"],\n importance: 0.74,\n meta: { count: out.length }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"route-brief\",\n text: out.map((x) => `${x.name}:${String(x.reply || \"\").slice(0, 140)}`).join(\" | \").slice(0, 1400),\n roomId: \"studio-main\",\n meta: {\n waypointCount: out.length,\n waypoints: out.map((x) => ({\n name: x.name,\n persona: x.persona,\n lat: x.place?.lat ? Number(x.place.lat) : null,\n lon: x.place?.lon ? Number(x.place.lon) : null\n }))\n }\n });\n sendJson(res, 200, { ok: true, items: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"route_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/pdf-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const title = String(payload?.title || \"document\");\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const providedText = String(payload?.text || \"\");\n const url = String(payload?.url || \"\");\n let content = providedText;\n if (!content && url) content = await fetchTextFromUrl(url);\n if (!content) return sendJson(res, 400, { ok: false, error: \"text_or_supported_url_required\" });\n const summary = await queryRhizohLlm(\n {\n message: `Summarize this document and provide key points + practical actions.\\nTitle:${title}\\nContent:\\n${content.slice(0, 12000)}`,\n context: { source: \"event-layer-pdf-brief\", title },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `DOC_BRIEF:${title} => ${String(summary.reply || \"\").slice(0, 1000)}`,\n tags: [\"document\", \"brief\", \"library\", \"event-layer\"],\n importance: 0.7,\n meta: { title, url: url || null }\n });\n await addTranscript(auth.uid, {\n source: \"library-brief\",\n eventType: \"pdf-brief\",\n text: `${title} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { title }\n });\n sendJson(res, 200, { ok: true, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/pdf/upload\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req, 8 * 1024 * 1024);\n const job = await enqueuePdfJob({\n uid: auth.uid,\n fileName: payload?.fileName || \"document.pdf\",\n bytesBase64: payload?.contentBase64\n });\n sendJson(res, 200, { ok: true, job });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_upload_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/session\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createPublishSession(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session\",\n text: `Publish session armed: ${row.protocol} ${row.target}`.slice(0, 500),\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: row.id, bridge: row.bridge }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/studio/publish/sessions\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listPublishSessions(auth.uid, 80);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/health-check\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const protocol = String(payload?.protocol || \"WHIP\");\n const target = String(payload?.target || \"\");\n const health = await checkPublishTargetHealth(protocol, target);\n if (payload?.sessionId) {\n await updatePublishSession(auth.uid, String(payload.sessionId), {\n status: health.ok ? \"healthy\" : \"degraded\",\n reconnectPolicy: {\n enabled: true,\n backoffSec: [2, 5, 10, 20],\n nextRetryInSec: health.reconnectInSec || 0,\n maxRetries: 8,\n reason: health.reason\n }\n });\n }\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-health\",\n text: `${protocol} ${target} => ${health.ok ? \"healthy\" : \"degraded\"} (${health.reason})`,\n roomId: \"studio-main\",\n meta: { protocol, target, reconnect: !!health.shouldReconnect }\n });\n sendJson(res, 200, { ok: true, health });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_health_check_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/studio/publish/sessions/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updatePublishSession(auth.uid, id, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session-update\",\n text: `Session ${id} -> ${row.status || \"updated\"}`,\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: id, status: row.status, ingestUrl: row.ingestUrl || \"\" }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/transcripts\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/transcripts\"}`);\n const limit = Math.max(1, Math.min(200, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await listTranscripts(auth.uid, limit);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/transcripts\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await addTranscript(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"transcript_write_failed\" });\n }\n return;\n }\n\n /** E1: broadcast intent → GreenRoom capability → transcript + memory artifact */\n if (req.method === \"POST\" && req.url === \"/studio/capabilities/greenroom\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const traceId = String(payload?.traceId || `TRC-${Date.now().toString(36).toUpperCase()}`);\n const title = String(payload?.title || \"Castle Live Broadcast\").slice(0, 200);\n const intentRaw = String(payload?.intentRaw || \"\").slice(0, 800);\n const audienceEstimate = Math.max(0, Math.min(50_000_000, Number(payload?.audienceEstimate) || 1200));\n const roomId = String(payload?.roomId || \"greenroom-main\");\n const transcriptRow = await addTranscript(auth.uid, {\n source: \"rhizoh\",\n eventType: \"BROADCAST_ROUTED\",\n text: `GreenRoom · ${title}${intentRaw ? `\\nIntent: ${intentRaw}` : \"\"}`,\n roomId,\n meta: {\n ack: \"BROADCAST_ROUTED\",\n capability: \"greenroom\",\n intentType: \"broadcast\",\n traceId,\n audienceEstimate,\n artifact: {\n kind: \"broadcast_card\",\n title,\n traceId,\n roomId,\n createdAt: Date.now()\n }\n }\n });\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n text: `GREENROOM_BROADCAST:${title} trace=${traceId}`,\n tags: [\"broadcast\", \"greenroom\", \"artifact\", \"castle-library\"],\n importance: 0.72,\n kind: \"episodic\",\n meta: { traceId, capability: \"greenroom\", transcriptId: transcriptRow.id }\n });\n sendJson(res, 200, {\n ok: true,\n ack: \"BROADCAST_ROUTED\",\n traceId,\n transcript: transcriptRow,\n replayPath: `/replay/${encodeURIComponent(traceId)}`,\n sharePath: `/replay/${encodeURIComponent(traceId)}`\n });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"greenroom_capability_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/metadata/search\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/metadata/search\"}`);\n const q = String(url.searchParams.get(\"q\") || \"\");\n const limit = Math.max(1, Math.min(300, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await searchMetadata(auth.uid, q, limit);\n sendJson(res, 200, { ok: true, q, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerDevice(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, device: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"device_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const lat = Number(payload?.lat);\n const lon = Number(payload?.lon);\n const speed = Math.max(0, Number(payload?.speed || 0));\n const cfgCenterLat = Number(process.env.CASTLE_GEOFENCE_CENTER_LAT || 41.0082);\n const cfgCenterLon = Number(process.env.CASTLE_GEOFENCE_CENTER_LON || 28.9784);\n const cfgRadiusM = Math.max(100, Number(process.env.CASTLE_GEOFENCE_RADIUS_M || 55000));\n const cfgSpeedCap = Math.max(1, Number(process.env.CASTLE_SPEED_CAP_MPS || 35));\n const dist = Number.isFinite(lat) && Number.isFinite(lon) ? haversineMeters(cfgCenterLat, cfgCenterLon, lat, lon) : 0;\n const geofenceOk = !Number.isFinite(dist) ? true : dist <= cfgRadiusM;\n const speedOk = speed <= cfgSpeedCap;\n const failsafe = !(geofenceOk && speedOk);\n const safety = {\n geofenceOk,\n speedOk,\n failsafe,\n distanceFromCenterM: Number.isFinite(dist) ? dist : null,\n speedCapMps: cfgSpeedCap\n };\n const row = await appendTelemetry(auth.uid, deviceId, {\n lat,\n lon,\n speed,\n battery: payload?.battery,\n mode: payload?.mode || (failsafe ? \"FAILSAFE_HOLD\" : \"ACTIVE\"),\n raw: payload?.raw || {}\n });\n await updateDevice(auth.uid, deviceId, {\n status: failsafe ? \"failsafe\" : \"active\",\n lastTelemetryAt: row.ts,\n safetyState: safety\n });\n if (failsafe) {\n await addTranscript(auth.uid, {\n source: \"robotics-guardrails\",\n eventType: \"failsafe\",\n text: `Device ${deviceId} failsafe triggered geofenceOk=${geofenceOk} speedOk=${speedOk}`,\n roomId: \"studio-main\",\n meta: { deviceId, safety }\n });\n }\n sendJson(res, 200, { ok: true, telemetry: row, safety, command: failsafe ? \"FAILSAFE_HOLD\" : \"CONTINUE\" });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"telemetry_ingest_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listTelemetry(auth.uid, 150);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createAutomation(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"automation-scheduled\",\n text: `Automation scheduled: ${row.title} at ${new Date(row.scheduleAt).toLocaleString()}`,\n roomId: \"studio-main\",\n meta: { automationId: row.id, action: row.action, deviceId: row.deviceId }\n });\n sendJson(res, 200, { ok: true, automation: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"automation_create_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAutomations(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listDevices(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const devs = await listDevices(auth.uid, 500);\n const d = devs.find((x) => x.id === deviceId);\n if (!d) return sendJson(res, 404, { ok: false, error: \"device_not_found\" });\n const action = String(payload?.action || \"\");\n const caps = Array.isArray(d.capabilityProfile) ? d.capabilityProfile : [];\n if (action && caps.length && !caps.includes(action) && !caps.includes(\"*\")) {\n return sendJson(res, 400, { ok: false, error: \"unsupported_action_for_device\", capabilityProfile: caps });\n }\n const cmd = await enqueueCommand(auth.uid, {\n deviceId,\n action: action || \"NOOP\",\n params: payload?.params || {},\n adapter: payload?.adapter || d.adapter || \"websocket\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-command-queue\",\n eventType: \"command-queued\",\n text: `${cmd.deviceId} -> ${cmd.action}`,\n roomId: \"studio-main\",\n meta: { commandId: cmd.id, adapter: cmd.adapter }\n });\n sendJson(res, 200, { ok: true, command: cmd });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listCommands(auth.uid, 200);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/robotics/commands/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updateCommandStatus(auth.uid, id, payload?.status || \"sent\");\n sendJson(res, 200, { ok: true, command: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/rhizoh/bridge\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const result = await runRhizohBrainV2({\n message: String(payload?.message || \"robotics bridge command\"),\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n worldState: payload?.worldState || {},\n deviceState: payload?.deviceState || {},\n intent: \"robotics-mechanics\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-bridge\",\n eventType: \"robotics-command\",\n text: `Bridge -> ${String(result.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { directive: result.directive, agentId: payload?.agentId || \"\" }\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"robotics_bridge_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/planner/graph\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const graph = buildTaskActionGraph(String(payload?.task || \"\"), payload?.context || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"planner-graph\",\n text: `Planner graph built: ${graph.taskTitle} nodes=${graph.nodes.length}`,\n roomId: \"studio-main\",\n meta: { taskTitle: graph.taskTitle, nodeCount: graph.nodes.length }\n });\n sendJson(res, 200, { ok: true, graph });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"planner_graph_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerSocialChannel(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, channel: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_channel_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listSocialChannels(auth.uid, 100);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/broadcast\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const text = String(payload?.text || \"\").slice(0, 1200);\n const channels = await listSocialChannels(auth.uid, 100);\n const enabled = channels.filter((c) => c.status === \"enabled\");\n const deliveries = [];\n for (const c of enabled) {\n const out = await deliverSocialWithRetry(c, text);\n deliveries.push({ id: c.id, platform: c.platform, ...out });\n }\n const okCount = deliveries.filter((d) => d.ok).length;\n await addTranscript(auth.uid, {\n source: \"social-layer\",\n eventType: \"social-broadcast\",\n text: `Broadcast delivered ${okCount}/${enabled.length} channel(s): ${text.slice(0, 240)}`,\n roomId: \"studio-main\",\n meta: { deliveries }\n });\n sendJson(res, 200, { ok: true, sentChannels: okCount, totalChannels: enabled.length, deliveries });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_broadcast_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/ethics/programs\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, ...buildEthicsPrograms() });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands/dispatch\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const all = await listCommands(auth.uid, 200);\n const queued = all.filter((c) => String(c.status || \"queued\") === \"queued\").slice(0, 50);\n const devices = await listDevices(auth.uid, 500);\n const byId = new Map(devices.map((d) => [d.id, d]));\n const results = [];\n for (const cmd of queued) {\n const dev = byId.get(cmd.deviceId);\n if (!dev) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: \"device_not_found\" });\n continue;\n }\n try {\n const dispatched = await dispatchCommandToAdapter(dev, cmd);\n await updateCommandStatus(auth.uid, cmd.id, \"sent\");\n results.push({ id: cmd.id, ok: true, adapter: dispatched.adapter, via: dispatched.via });\n } catch (error) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: String(error?.message || \"dispatch_failed\") });\n }\n }\n await addTranscript(auth.uid, {\n source: \"robotics-command-dispatch\",\n eventType: \"command-dispatch-batch\",\n text: `Dispatch batch size=${results.length} ok=${results.filter((x) => x.ok).length}`,\n roomId: \"studio-main\",\n meta: { results: results.slice(0, 30) }\n });\n sendJson(res, 200, { ok: true, processed: results.length, results });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"dispatch_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/pdf/jobs/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n const job = await getPdfJob(auth.uid, id);\n if (!job) return sendJson(res, 404, { ok: false, error: \"pdf_job_not_found\" });\n sendJson(res, 200, { ok: true, job });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const row = await appendMemory({\n scope,\n id,\n text: payload?.text,\n tags: payload?.tags || [],\n importance: payload?.importance,\n kind: payload?.kind || \"episodic\",\n meta: { uid: auth.uid, ...(payload?.meta || {}) }\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.externalTruth) {\n try {\n if (process.env.RHIZOH_EXTERNAL_TRUTH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_truth:${ip}`, RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n sendJson(res, 200, buildRhizohExternalGroundTruthPayload(req));\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.productOutcome) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const rawBody = await readHttpBodyText(req, 64 * 1024);\n const body = rawBody.trim() ? safeJsonParse(rawBody) : null;\n if (!body || typeof body !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"invalid_json_payload\" });\n return;\n }\n const sigHdr = String(req.headers?.[\"x-rhizoh-outcome-signature\"] || \"\").trim();\n const hmacV = verifyRhizohOutcomeHmac(rawBody, sigHdr);\n if (!hmacV.ok) {\n sendJson(res, 401, { ok: false, error: hmacV.error || \"outcome_hmac_invalid\" });\n return;\n }\n const srcTokHdr = String(req.headers?.[\"x-rhizoh-outcome-source-token\"] || \"\").trim();\n const srcV = verifyRhizohOutcomeSourceToken(body.source, srcTokHdr);\n if (!srcV.ok) {\n sendJson(res, 401, { ok: false, error: srcV.error || \"outcome_source_token_invalid\" });\n return;\n }\n const subjectKey = String(body.subjectKey || \"\").trim().slice(0, 128);\n if (\n subjectKey &&\n !checkHttpRateLimit(\n `rhizoh_product_outcome_subj:${subjectKey}`,\n RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN,\n 60_000\n )\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_subject\" });\n return;\n }\n const result = ingestRhizohProductOutcomeHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n schemaVersion: \"1.1.0\",\n aggregate: result.aggregate,\n integrity: result.integrity\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"outcome_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.productOutcomeAggregate) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome_agg:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const u = new URL(String(req.url || \"/\"), \"http://localhost\");\n const cohortId = u.searchParams.get(\"cohortId\") || \"\";\n const decisionFingerprint = u.searchParams.get(\"decisionFingerprint\") || \"\";\n const limit = u.searchParams.get(\"limit\") || \"50\";\n const rows = listRhizohProductOutcomeAggregates({\n cohortId: cohortId || undefined,\n decisionFingerprint: decisionFingerprint || undefined,\n limit: Number(limit) || 50\n });\n sendJson(res, 200, { ok: true, schemaVersion: \"1.1.0\", rows });\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.externalLossBatch) {\n try {\n if (process.env.RHIZOH_EXTERNAL_LOSS_BATCH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_loss_batch:${ip}`, RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const result = ingestRhizohExternalLossBatchHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, { ok: true, accepted: result.accepted ?? 0 });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"batch_failed\") });\n }\n return;\n }\n\n function moderationAdminOk(req) {\n const expected = String(process.env.CASTLE_MODERATION_ADMIN_KEY || \"\").trim();\n if (!expected) return false;\n const got = String(req.headers[\"x-castle-moderation-key\"] || \"\").trim();\n return got.length > 0 && got === expected;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.opsAbuseReport) {\n try {\n const body = await readHttpJson(req, 32 * 1024);\n const auth = await resolveHttpUser(req);\n const entry = submitAbuseReportV0({\n reporterUid: auth.ok ? auth.uid : null,\n targetUid: body?.targetUid ?? null,\n category: body?.category,\n detail: body?.detail,\n traceId: body?.traceId ?? null\n });\n sendJson(res, 202, { ok: true, reportId: entry.id, status: entry.status });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsModerationQueue) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n queue: listModerationQueueV0(100),\n softBlocked: listSoftBlockedUsersV0()\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-block\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const out = softBlockUserV0(body?.uid, body?.reason);\n sendJson(res, out.ok ? 200 : 400, out);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-unblock\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n sendJson(res, 200, removeSoftBlockV0(body?.uid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsAgentSnapshots) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n const limit = Number(new URL(req.url || \"\", \"http://local\").searchParams.get(\"limit\") || 50);\n sendJson(res, 200, { ok: true, snapshots: listRecentAgentSnapshotsV0(limit) });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsHardeningStatus) {\n const auth = await resolveHttpUser(req);\n const principal = auth.ok ? `uid:${auth.uid}` : null;\n const gclSnap = await getGlobalCostLedgerSnapshotV0(principal);\n const economicStrategy = evaluateEconomicStrategyL3V0(gclSnap);\n const hardeningTenantId = resolveHardeningTenantIdV0(auth, req);\n const unifiedState = await buildUnifiedStateNarrativeV0({\n tenantId: hardeningTenantId ?? undefined,\n platformScope: !hardeningTenantId,\n principal\n });\n sendJson(res, 200, {\n ok: true,\n schema: \"rhizoh.ops.hardening_status.v0\",\n containment: readAgentContainmentConfigV0(),\n cost: readCostContainmentConfigV0(),\n gcl: {\n backend: await getGclBackendStatusV0(),\n snapshot: gclSnap\n },\n economicStrategyL3: economicStrategy,\n phasedRollout: await getPhasedRolloutStatsV0(),\n tier: readPhasedRolloutTierV0(),\n principalCost: auth.ok ? await getCostContainmentStatsV0(principal) : null,\n stressTaxonomy: verifyStressTaxonomyCoverageV0(),\n stressConflictResolution: verifyStressConflictResolutionV0(),\n resolutionStabilityEnvelope: verifyResolutionStabilityEnvelopeV0(),\n unifiedState: {\n tenantScope: {\n tenantId: unifiedState.tenantScope?.tenantId,\n scope: unifiedState.tenantScope?.scope,\n noGlobalDerivedState: unifiedState.tenantScope?.noGlobalDerivedState\n },\n narrativeFingerprint: unifiedState.narrativeFingerprint,\n screenshotScopeWatermark: unifiedState.screenshotScopeWatermark,\n interpretationSafetyContract: unifiedState.interpretationSafetyContract,\n interpretationUxContract: {\n schema: unifiedState.interpretationUxContract?.schema,\n layerOrder: unifiedState.interpretationUxContract?.layerOrder,\n narrativeNeverVisuallyDominant: unifiedState.interpretationUxContract?.narrativeNeverVisuallyDominant\n },\n governance: {\n narrativeIsNotDecisionLayer: unifiedState.governance.narrativeIsNotDecisionLayer,\n displayOrder: unifiedState.governance.displayOrder,\n trustPosture: unifiedState.governance.trustPosture,\n interpretationUxContract: unifiedState.governance.interpretationUxContract?.schema\n },\n humanOps: {\n decisionTier: unifiedState.humanOps?.humanDecisionScaling?.tier,\n decisionOwner: unifiedState.humanOps?.humanDecisionScaling?.decisionOwner,\n opsRouteId: unifiedState.humanOps?.humanDecisionOpsRunbook?.routing?.routeId,\n slaAckMinutes: unifiedState.humanOps?.humanDecisionOpsRunbook?.sla?.ackMinutes,\n uiDisclaimerRequired: unifiedState.humanOps?.narrativeUiSafety?.displayRules?.requireDisclaimerBanner,\n misreadHighResidual: unifiedState.humanOps?.misreadSimulation?.highResidualCount,\n propagationHighResidual: unifiedState.humanOps?.socialPropagationSimulation?.highResidualCount,\n worstWatermarkSurvivability:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.worstWatermarkSurvivability,\n dominantDistortionSource:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.dominantDistortionSource\n },\n culturalRisk: {\n perceptualRiskClass: unifiedState.culturalRisk?.perceptualRiskClass,\n trustFork: unifiedState.culturalRisk?.trustDynamics?.fork,\n trustDecayScore: unifiedState.culturalRisk?.trustDynamics?.scores?.trustDecay,\n mythologyScore: unifiedState.culturalRisk?.trustDynamics?.scores?.mythology,\n confidenceDetachmentExposure:\n unifiedState.culturalRisk?.trustDynamics?.axes?.confidenceDetachmentExposure\n },\n appliedSystemsLayer: {\n schema: unifiedState.appliedSystemsLayer?.schema,\n researchOutputType: unifiedState.appliedSystemsLayer?.researchGovernance?.outputType,\n roboticsBlockActuation: unifiedState.appliedSystemsLayer?.roboticsGrounding?.blockActuation,\n spiralRumorCount:\n unifiedState.appliedSystemsLayer?.spiralMMOGameKernel?.gameLayer?.rumors?.length\n },\n actionSemanticGovernance: {\n schema: unifiedState.actionSemanticGovernance?.schema,\n highRiskActionCount: unifiedState.actionSemanticGovernance?.highRiskActionCount,\n loopClosed: unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.loopClosed,\n authorityCollapseMitigated:\n unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.risks?.authorityCollapse\n ?.mitigated\n },\n actionContextResolution: {\n schema: unifiedState.actionContextResolution?.schema,\n contextFingerprint: unifiedState.actionContextResolution?.contextFingerprint?.fingerprintHash,\n domainCollision: unifiedState.actionContextResolution?.domainCollision?.detected,\n deployAgentSpiralBlocked:\n unifiedState.actionContextResolution?.actionOverloadingRisk?.deployAgentExample\n ?.spiral_mmo === \"blocked\",\n roboticsActuationEligible:\n unifiedState.actionContextResolution?.roboticsActuationCorrectness?.actuationEligible\n },\n epistemicCoherence: {\n schema: unifiedState.epistemicCoherence?.schema,\n verdict: unifiedState.epistemicCoherence?.systemCoherence?.verdict,\n coherenceScore: unifiedState.epistemicCoherence?.systemCoherence?.score,\n contradictionCount: unifiedState.epistemicCoherence?.crossLayerContradictions?.count,\n operatorDecision: unifiedState.epistemicCoherence?.uxCompression?.operatorDecision,\n headlineTr: unifiedState.epistemicCoherence?.uxCompression?.headline?.tr,\n falseSafetyWarning: Boolean(\n unifiedState.epistemicCoherence?.uxCompression?.falseSafetyWarning\n )\n },\n coherenceAuthorityBoundary: {\n schema: unifiedState.coherenceAuthorityBoundary?.schema,\n bindingValid: unifiedState.coherenceAuthorityBoundary?.eclAuthorityBinding?.valid,\n canExecute: unifiedState.coherenceAuthorityBoundary?.contract?.can_execute,\n mandatoryBannerTr:\n unifiedState.coherenceAuthorityBoundary?.disclaimers?.mandatoryBanner?.tr\n },\n decisionLatencyGovernance: {\n schema: unifiedState.decisionLatencyGovernance?.schema,\n latencyTier: unifiedState.decisionLatencyGovernance?.latencyTier,\n inflationScore: unifiedState.decisionLatencyGovernance?.latencyInflation?.score,\n fastPathEligible: unifiedState.decisionLatencyGovernance?.fastPath?.eligible,\n primaryActionId: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.primaryActionId,\n ackSlaMinutes: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.ackSlaMinutes,\n certaintyCap: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.certaintyCap,\n uncertaintyItems:\n unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.uncertaintyEnvelope?.itemCount\n },\n decisionPacketUncertaintyBoundary: {\n schema: unifiedState.decisionPacketUncertaintyBoundary?.schema,\n invariantValid: unifiedState.decisionPacketUncertaintyBoundary?.invariantCheck?.valid,\n centralInvariant: unifiedState.decisionPacketUncertaintyBoundary?.centralInvariant\n },\n epistemicDecisionPacing: {\n schema: unifiedState.epistemicDecisionPacing?.schema,\n queueSaturationTier:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation?.tier,\n operatorProcessingLatency:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation\n ?.operatorProcessingLatency,\n postureWindowCount:\n unifiedState.epistemicDecisionPacing?.temporaryStaticPostureWindows\n ?.temporaryStaticPostureWindows?.windowCount,\n executionBoundariesValid:\n unifiedState.epistemicDecisionPacing?.executionBoundaries?.valid,\n grantsExecutionApproval: unifiedState.epistemicDecisionPacing?.grantsExecutionApproval\n },\n epistemicTemporalCoherence: {\n schema: unifiedState.epistemicTemporalCoherence?.schema,\n allInvariantsValid: unifiedState.epistemicTemporalCoherence?.allInvariantsValid,\n temporalAlignmentValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.temporalAlignment?.valid,\n crossWindowGuardValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard?.valid,\n delayedTruthValid: unifiedState.epistemicTemporalCoherence?.invariants?.delayedTruthRule?.valid,\n reconciliationRequired:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard\n ?.reconciliation?.required\n },\n realityDriftObserver: {\n schema: unifiedState.realityDriftObserver?.schema,\n driftActive: unifiedState.realityDriftObserver?.driftObserverSummary?.active,\n misapprehensionShapeCount:\n unifiedState.realityDriftObserver?.misapprehensionShapeCatalog?.shapeCount,\n criticalShapeCount:\n unifiedState.realityDriftObserver?.driftObserverSummary?.criticalShapeCount,\n roboticsMismatchActive:\n unifiedState.realityDriftObserver?.roboticsFeedbackMismatch?.active\n },\n driftCausality: {\n schema: unifiedState.driftCausality?.schema,\n shapesWithCausalExplanation: unifiedState.driftCausality?.rdolLinkage?.shapesWithCausalExplanation,\n activeDomainCount: unifiedState.driftCausality?.causalitySummary?.activeDomainCount,\n propagationCausalityActive: unifiedState.driftCausality?.causalDomains?.propagation?.active\n },\n productReadiness: unifiedState.productReadiness,\n stateLayers: {\n raw: unifiedState.stateLayers.raw,\n derived: {\n systemState: unifiedState.stateLayers.derived.systemState,\n narrative: unifiedState.stateLayers.derived.narrative,\n validation: unifiedState.stateLayers.derived.validation\n },\n policy: {\n binding: unifiedState.stateLayers.policy.binding,\n decisionOwner: unifiedState.stateLayers.policy.decisionOwner,\n prohibitedActions: unifiedState.stateLayers.policy.prohibitedActions\n }\n },\n systemState: unifiedState.systemState,\n interpretation: unifiedState.interpretation,\n validation: {\n trustPosture: unifiedState.validation.trustPosture,\n adjustedConfidence: unifiedState.validation.adjustedConfidence,\n agreement: unifiedState.validation.confidenceDecomposition.agreement,\n divergenceScore: unifiedState.validation.divergence.divergenceScore,\n validated: unifiedState.validation.divergence.validated,\n uncertaintyTags: unifiedState.validation.uncertainty.tags.map((t) => t.id),\n flags: unifiedState.validation.divergence.flags.map((f) => f.id)\n },\n compression: unifiedState.compression\n }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3) {\n try {\n const payload = await readHttpJson(req, 6 * 1024 * 1024);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (\n !checkHttpRateLimit(`rhizoh_voice_transcribe:${rlKey}`, RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN, 60_000)\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const voiceEnv = rhizohVoiceTranscribeEnvV3();\n if (!voiceEnv.any) {\n sendJson(res, 503, {\n ok: false,\n error: \"voice_asr_not_configured\",\n env: voiceEnv\n });\n return;\n }\n const result = await runRhizohVoiceTranscribeV3(payload);\n sendJson(res, result.ok ? 200 : 422, {\n ...result,\n traceId: String(payload?.traceId || \"\"),\n atMs: Date.now()\n });\n } catch (error) {\n sendJson(res, 500, {\n ok: false,\n error: \"voice_transcribe_failed\",\n detail: String(error?.message || error)\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === rhizohRuntime.routes.rhizohLlm) {\n try {\n const payload = await readHttpJson(req);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n logRhizohHealth(\"gateway_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: String(payload?.traceId || \"\"),\n auth: auth.ok ? \"ok\" : \"anon\",\n ip\n });\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (!checkHttpRateLimit(`rhizoh_llm:${rlKey}`, RL_RHIZOH_LLM_PER_MIN, 60_000)) {\n const rlTaxonomy = classifyStressResponseV0({ code: \"rate_limit_exceeded\" });\n return sendJson(res, 429, {\n ok: false,\n error: \"rate_limit_exceeded\",\n reply: rlTaxonomy.userMessageTr || \"İstek sınırı aşıldı. Kısa süre sonra tekrar deneyin.\",\n stressClass: rlTaxonomy.stressClass,\n responseAction: rlTaxonomy.responseAction,\n stressMatrix: rlTaxonomy.matrix,\n stressInterpretable: rlTaxonomy.interpretable,\n stressConfidence: rlTaxonomy.stressConfidence,\n actionConfidence: rlTaxonomy.actionConfidence,\n actionSoftened: rlTaxonomy.actionSoftened,\n responseActionStrict: rlTaxonomy.responseActionStrict,\n actionInterpretable: rlTaxonomy.actionInterpretable,\n conflictResolution: rlTaxonomy.conflictResolution,\n directive: \"NONE\"\n });\n }\n\n const requireExplicit = process.env.CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE === \"1\";\n const keyMode = normalizeClientLlmKeySource(payload?.llmKeySource ?? payload?.keySource);\n if (keyMode === null) {\n return sendJson(res, 400, {\n ok: false,\n error: \"invalid_llm_key_source\",\n reply: \"llmKeySource geçersiz. Kullanın: env | user_connection | auto\",\n directive: \"NONE\"\n });\n }\n if (requireExplicit && keyMode === \"auto\") {\n return sendJson(res, 400, {\n ok: false,\n error: \"llm_key_source_required\",\n reply: \"Sunucu CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE=1: istekte llmKeySource (env veya user_connection) zorunlu.\",\n directive: \"NONE\"\n });\n }\n\n const conn = auth.ok ? await resolveConnection(auth.uid, String(payload?.connectionId || \"\")) : null;\n\n if (keyMode === \"user_connection\") {\n if (!auth.ok) {\n return sendJson(res, 401, {\n ok: false,\n error: \"llm_auth_required_for_user_connection\",\n reply: \"Kayıtlı LLM bağlantısı için giriş gerekli.\",\n directive: \"NONE\"\n });\n }\n if (!conn?.apiKey) {\n return sendJson(res, 400, {\n ok: false,\n error: \"user_llm_connection_required\",\n reply: \"Geçerli bir LLM bağlantısı yok; Studio’da bağlantı ekleyin veya llmKeySource=env kullanın.\",\n directive: \"NONE\"\n });\n }\n }\n\n const safePayload = { ...(payload || {}) };\n delete safePayload.apiKey;\n delete safePayload.llmKeySource;\n delete safePayload.keySource;\n\n let resolvedProvider = payload?.provider;\n let resolvedModel = payload?.model;\n let connApiKey = \"\";\n\n if (keyMode === \"user_connection\") {\n resolvedProvider = conn.provider;\n resolvedModel = conn.model;\n connApiKey = conn.apiKey || \"\";\n } else if (keyMode === \"env\") {\n connApiKey = \"\";\n } else {\n resolvedProvider = conn?.provider || payload?.provider;\n resolvedModel = conn?.model || payload?.model;\n connApiKey = conn?.apiKey || \"\";\n }\n\n const { result, traceId, turnLatencyMs, spinePhases, sampledTrace } = await rhizohGatewayTurn({\n safePayload,\n auth,\n keyMode,\n conn,\n resolvedProvider,\n resolvedModel,\n connApiKey\n });\n logRhizohHealth(\"llm_response\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId,\n turnLatencyMs: Number(turnLatencyMs || 0),\n keyMode\n });\n sendJson(res, 200, {\n ...result,\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n traceId,\n turnLatencyMs,\n ...(spinePhases ? { spinePhases } : {}),\n ...(sampledTrace !== undefined ? { sampledTrace } : {})\n });\n } catch (error) {\n const msg = String(error?.message || \"\");\n const code = error?.code || \"\";\n let status = 500;\n if (msg === \"rate_limit_exceeded\") status = 429;\n else if (\n code === \"cost_hard_limit\" ||\n code === \"phased_rollout_capacity\" ||\n error?.containment === true ||\n String(code).startsWith(\"agent_\")\n ) {\n status = 429;\n } else if (code === \"user_soft_blocked\" || code === \"prompt_abuse_detected\") status = 403;\n else if ([\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(code) || msg.includes(\"missing_api_key_for_\"))\n status = 400;\n let rhizohFailureKind = \"provider_error\";\n if (msg === \"rate_limit_exceeded\" || status === 429) rhizohFailureKind = \"rate_limit\";\n else if (status === 403) rhizohFailureKind = \"policy_block\";\n else if (status === 400) rhizohFailureKind = \"client_config\";\n else if (msg.startsWith(\"provider_http_\")) {\n const st = Number(msg.replace(\"provider_http_\", \"\"));\n if (st === 408 || st === 504) rhizohFailureKind = \"timeout\";\n else rhizohFailureKind = \"provider_error\";\n } else if (/timeout|timed out|ETIMEDOUT/i.test(msg)) rhizohFailureKind = \"timeout\";\n\n const providerHttpMatch = msg.match(/^provider_http_(\\d{3})$/);\n const stressTaxonomy =\n error?.stressClass != null\n ? {\n stressClass: error.stressClass,\n responseAction: error.responseAction,\n stressMatrix: error.stressMatrix,\n stressInterpretable: error.stressInterpretable !== false,\n stressConfidence: error.stressConfidence,\n actionConfidence: error.actionConfidence,\n actionSoftened: error.actionSoftened,\n responseActionStrict: error.responseActionStrict,\n actionInterpretable: error.actionInterpretable,\n conflictResolution: error.conflictResolution,\n stressSecondary: error.stressSecondary,\n responseActions: error.responseActions\n }\n : (() => {\n const t = classifyStressResponseV0({\n code: code || msg,\n rhizohFailureKind,\n providerHttpStatus: providerHttpMatch\n ? Number(providerHttpMatch[1])\n : null\n });\n return {\n stressClass: t.stressClass,\n responseAction: t.responseAction,\n stressMatrix: t.matrix,\n stressInterpretable: t.interpretable,\n stressConfidence: t.stressConfidence,\n actionConfidence: t.actionConfidence,\n actionSoftened: t.actionSoftened,\n responseActionStrict: t.responseActionStrict,\n actionInterpretable: t.actionInterpretable,\n conflictResolution: t.conflictResolution,\n stressSecondary: t.stressSecondary,\n responseActions: t.responseActions\n };\n })();\n sendJson(res, status, {\n ok: false,\n rhizohFailureKind,\n ...(providerHttpMatch ? { providerHttpStatus: Number(providerHttpMatch[1]) } : {}),\n error: code || msg || \"rhizoh_llm_failed\",\n reply:\n error?.reply ||\n (status === 500 ? \"Rhizoh bağlantısı geçici olarak kesildi.\" : msg),\n directive: error?.directive || \"NONE\",\n ...stressTaxonomy\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicSeal) {\n try {\n if (!EPISTEMIC_SEAL_SECRET) {\n sendJson(res, 503, { ok: false, error: \"epistemic_seal_disabled\" });\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`epistemic_seal:${ip}`, RL_EPISTEMIC_SEAL_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const truth = body.truth_contract;\n if (!truth || typeof truth !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"truth_contract_required\" });\n return;\n }\n const sealBody = {\n truth_contract: truth,\n runtime_hash: String(body.runtime_hash || \"\"),\n model_route: {\n provider: body.model_route?.provider ?? null,\n model: body.model_route?.model ?? null\n },\n memory_digest: String(body.memory_digest || \"\"),\n world_snapshot_hash: String(body.world_snapshot_hash || \"\"),\n timestamp: Number(body.timestamp) || Date.now()\n };\n const canonical = canonicalEpistemicSealString(sealBody);\n const { hash, signature } = hashAndSignEpistemicSeal(canonical, EPISTEMIC_SEAL_SECRET);\n recordGenesisEpistemicSealIssued(hash);\n const attestation = {\n schema: EPISTEMIC_SEAL_SCHEMA,\n algorithm: \"SHA-256+HMAC-SHA256\",\n keyHint: process.env.CASTLE_EPISTEMIC_SEAL_SECRET ? \"CASTLE_EPISTEMIC_SEAL_SECRET\" : \"CASTLE_GATEWAY_TOKEN\",\n issuedAt: Date.now(),\n gateway: { service: \"castle-gateway\", listenPort: PORT }\n };\n sendJson(res, 200, {\n ok: true,\n hash,\n signature,\n attestation,\n canonicalBytes: Buffer.byteLength(canonical, \"utf8\")\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"seal_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicLogsBatch) {\n try {\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const auth = await resolveHttpUser(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`epistemic_logs:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 120) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const persisted = await persistEpistemicLedgerBatch(auth.uid, entries);\n recordGenesisEpistemicLedgerPersisted(persisted.persisted);\n // Internal cognition layer: forecast stays gateway/ledger-only (no client exposure).\n try {\n await persistEpistemicForecastBatch(auth.uid, persisted.normalized || []);\n } catch {\n /* forecast failures must not break primary ledger ingest */\n }\n sendJson(res, 200, {\n ok: true,\n persisted: persisted.persisted,\n mode: persisted.mode,\n latest: persisted.normalized?.[persisted.normalized.length - 1] || null\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"epistemic_log_ingest_failed\") });\n }\n return;\n }\n\n sendJson(res, 404, { ok: false, error: \"not_found\" });\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\nspiralWssForGenesis = wss;\nconst orchestrator = createOrchestrator();\nconst clientStats = new Map();\nlet broadcasterClientId = null;\nconst spiralState = {\n rooms: [{ id: \"room-default\", name: \"Spiral Default Arena\" }],\n activeByClient: new Map(),\n characters: new Map() // id -> {id,name,role,roomId,source}\n};\n\n/** Castle social rooms — keyed by `castleRoomKey` from {@link WS_MESSAGE.CASTLE_SOCIAL_PULSE}. */\nconst castleSocialRoomState = new Map();\nconst CASTLE_SOCIAL_MEMBER_TTL_MS = 90_000;\nconst CASTLE_SOCIAL_MAX_PULSE_BYTES = 8192;\n\nfunction removeCastleSocialClientFromAllRooms(clientId) {\n const cid = String(clientId || \"\");\n if (!cid) return;\n for (const room of castleSocialRoomState.values()) {\n room.members.delete(cid);\n }\n}\n\nfunction buildCastleSocialRoomPayload(roomKey) {\n const room = castleSocialRoomState.get(roomKey);\n if (!room) return null;\n const now = Date.now();\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const roster = [...room.members.values()].map((m) => ({\n userId: m.userId,\n lastMs: m.lastMs,\n ...m.sp\n }));\n const peerFeeds =\n room.walPeers && room.walPeers.size\n ? [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }))\n : [];\n return { castleRoomKey: roomKey, seq: room.seq, roster, peerFeeds, ts: now };\n}\n\nconst CASTLE_WAL_PEER_MAX_FEED_BYTES = 48 * 1024;\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {{ ok: boolean, code?: string, reason?: string }} gate\n */\nfunction rejectWalPeerIngressV0(socket, gate) {\n const code = gate.code || \"wal_peer_rejected\";\n recordWalPeerRejectV0(code);\n recordSubstrateOtelEventV0(\"wal_peer_reject\", { code, reason: String(gate.reason || \"\").slice(0, 120) });\n socket.send(\n JSON.stringify(\n createEnvelope(WS_MESSAGE.ERROR, {\n error: gate.reason || \"WAL peer ingress rejected.\",\n code\n })\n )\n );\n}\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {Record<string, unknown> | null | undefined} walPeerFeed\n * @returns {boolean}\n */\nfunction gateWalPeerIngressV0(socket, walPeerFeed) {\n const authGate = assertWalPeerSocketAuthorityV0(socket);\n if (!authGate.ok) {\n rejectWalPeerIngressV0(socket, authGate);\n return false;\n }\n const sigGate = validateWalPeerFeedSignatureV0(\n walPeerFeed && typeof walPeerFeed === \"object\" ? walPeerFeed : {}\n );\n if (!sigGate.ok) {\n rejectWalPeerIngressV0(socket, sigGate);\n return false;\n }\n return true;\n}\n\nfunction handleCastleWalPeerFeed(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_WAL_PEER_MAX_FEED_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_WAL_PEER_FEED too large.\" })));\n return;\n }\n const walPeerFeed = p.walPeerFeed && typeof p.walPeerFeed === \"object\" ? p.walPeerFeed : {};\n if (!gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const castleRoomKey = String(p.castleRoomKey || \"wal:convergence\").slice(0, 64);\n const castleId = String(p.castleId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId,\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: room.members.get(socket.clientId)?.sp ?? {}\n });\n const peerFeeds = [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }));\n const body = {\n castleRoomKey,\n seq: room.seq,\n peerFeeds,\n ts: now\n };\n const env = createEnvelope(WS_MESSAGE.CASTLE_WAL_PEER_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys(), ...room.walPeers.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction handleCastleSocialPulse(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_SOCIAL_MAX_PULSE_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_SOCIAL_PULSE too large.\" })));\n return;\n }\n const castleRoomKey = String(p.castleRoomKey || \"default\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n const sp = p.socialPulse && typeof p.socialPulse === \"object\" ? p.socialPulse : {};\n const walPeerFeed =\n p.walPeerFeed && typeof p.walPeerFeed === \"object\"\n ? p.walPeerFeed\n : sp.walPeerFeed && typeof sp.walPeerFeed === \"object\"\n ? sp.walPeerFeed\n : null;\n if (walPeerFeed && !gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const spNext = walPeerFeed ? { ...sp, walPeerFeed } : { ...sp };\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: spNext\n });\n if (walPeerFeed) {\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId: String(p.castleId || userId).slice(0, 64),\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n }\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const body = buildCastleSocialRoomPayload(castleRoomKey);\n if (!body) return;\n const env = createEnvelope(WS_MESSAGE.CASTLE_SOCIAL_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcast(msgObj) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcastState() {\n broadcast(\n createEnvelope(WS_MESSAGE.BROADCAST_STATE, {\n broadcasterClientId,\n viewers: [...wss.clients].filter((c) => c.readyState === 1).length\n })\n );\n}\n\nfunction currentSpiralStatePayload() {\n return {\n rooms: spiralState.rooms,\n characters: [...spiralState.characters.values()]\n };\n}\n\nfunction broadcastSpiralState() {\n broadcast(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload()));\n}\n\nfunction ensureRoom(roomId, fallbackName = null) {\n if (!roomId) return spiralState.rooms[0].id;\n const exists = spiralState.rooms.find((r) => r.id === roomId);\n if (exists) return exists.id;\n const created = { id: roomId, name: fallbackName || `Spiral-${roomId.slice(-4)}` };\n spiralState.rooms.push(created);\n return created.id;\n}\n\nfunction syncSpiralCharactersWithWorld(snapshot) {\n const agents = snapshot?.agents || [];\n for (const a of agents) {\n if (!spiralState.characters.has(a.id)) {\n spiralState.characters.set(a.id, {\n id: a.id,\n name: a.id,\n role: \"agent\",\n roomId: spiralState.rooms[0].id,\n source: \"world\"\n });\n }\n }\n}\n\nfunction broadcastExcept(msgObj, exceptSocket) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client !== exceptSocket && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction validateInputFrame(payload, socket) {\n const stats = clientStats.get(socket.clientId) || { lastFrame: 0, windowStart: Date.now(), msgCount: 0 };\n const now = Date.now();\n if (now - stats.windowStart > 5000) {\n stats.windowStart = now;\n stats.msgCount = 0;\n }\n stats.msgCount += 1;\n clientStats.set(socket.clientId, stats);\n if (stats.msgCount > 120) return \"Rate limit exceeded.\";\n\n if (!payload || !Number.isInteger(payload.frame)) return \"frame must be integer.\";\n if (payload.frame <= stats.lastFrame) return \"frame must be monotonic.\";\n const commands = Array.isArray(payload.commands) ? payload.commands : [];\n if (commands.length > 8) return \"Too many commands in one frame.\";\n for (const cmd of commands) {\n if (!cmd || typeof cmd.command !== \"string\") return \"Invalid command format.\";\n if (\n cmd.command !== \"SPAWN_AGENT\" &&\n cmd.command !== \"LIST_CASTLES\" &&\n cmd.command !== \"SPAWN_ENTITY\" &&\n cmd.command !== \"CREATE_CASTLE\"\n ) return \"Unsupported command.\";\n }\n stats.lastFrame = payload.frame;\n clientStats.set(socket.clientId, stats);\n return null;\n}\n\nfunction validateSignalPayload(payload) {\n if (!payload || typeof payload !== \"object\") return \"Invalid signaling payload.\";\n const signalType = payload.signalType;\n if (![\"OFFER\", \"ANSWER\", \"ICE\"].includes(signalType)) return \"Invalid signal type.\";\n if (typeof payload.to !== \"string\" || payload.to.length < 3) return \"Invalid signal destination.\";\n if (signalType !== \"ICE\" && JSON.stringify(payload.sdp || {}).length > 16 * 1024) return \"SDP too large.\";\n if (signalType === \"ICE\" && JSON.stringify(payload.candidate || {}).length > 6 * 1024) return \"ICE too large.\";\n return null;\n}\n\nfunction sanitizeRawMessage(raw) {\n if (!raw) return null;\n const text = raw.toString();\n if (Buffer.byteLength(text, \"utf8\") > MAX_MESSAGE_BYTES) return null;\n return text;\n}\n\nwss.on(\"connection\", async (socket, req) => {\n const origin = req?.headers?.origin || \"\";\n if (ALLOWED_ORIGINS.length && !ALLOWED_ORIGINS.includes(origin)) {\n socket.close(1008, \"Origin not allowed\");\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const path = req?.url || \"\";\n const query = new URL(`ws://localhost${path}`).searchParams;\n const token = query.get(\"token\");\n if (!token || token !== REQUIRED_GATEWAY_TOKEN) {\n socket.close(1008, \"Invalid gateway token\");\n return;\n }\n }\n if (REQUIRE_AUTH) {\n const result = await verifyClientToken(req);\n if (!result.ok) {\n const isLocal = origin.includes(\"localhost\") || origin.includes(\"127.0.0.1\") || !origin;\n if (!(ALLOW_DEV_ANON && isLocal)) {\n socket.close(1008, result.reason);\n return;\n }\n socket.auth = { ok: true, kind: \"dev-anon\", user: { uid: \"dev-anon\" } };\n } else {\n socket.auth = result;\n }\n }\n\n socket.clientId = `c-${Math.random().toString(36).slice(2, 8)}`;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.HELLO, { message: \"Castle Gateway online.\", clientId: socket.clientId })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.WORLD_SNAPSHOT, orchestrator.worldSnapshot())));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.BROADCAST_STATE, { broadcasterClientId, viewers: [...wss.clients].filter((c) => c.readyState === 1).length })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload())));\n broadcastExcept(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }), socket);\n\n socket.on(\"message\", (raw) => {\n const safeRaw = sanitizeRawMessage(raw);\n if (!safeRaw) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Message too large or invalid.\" })));\n return;\n }\n const parsed = safeJsonParse(safeRaw);\n if (!parsed?.type) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid message.\" })));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND_TEXT) {\n const canonical = parseTextToCommand(parsed.payload?.text);\n if (!canonical) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Unknown command text.\" })));\n return;\n }\n const result = orchestrator.applyCommand(canonical);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND) {\n const result = orchestrator.applyCommand(parsed.payload);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n }\n\n if (parsed.type === WS_MESSAGE.INPUT_FRAME) {\n const err = validateInputFrame(parsed.payload, socket);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const frame = parsed.payload.frame;\n const commands = Array.isArray(parsed.payload?.commands) ? parsed.payload.commands : [];\n orchestrator.queueInputFrame(socket.clientId, frame, commands);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, queued: true, frame })));\n }\n\n if (parsed.type === WS_MESSAGE.SIGNAL) {\n const payload = parsed.payload || {};\n const err = validateSignalPayload(payload);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const signalType = payload.signalType;\n const to = payload.to;\n for (const client of wss.clients) {\n if (client.clientId === to && client.readyState === 1) {\n client.send(JSON.stringify(createEnvelope(WS_MESSAGE.SIGNAL, { ...payload, from: socket.clientId })));\n break;\n }\n }\n }\n\n if (parsed.type === WS_MESSAGE.DRONE_TELEMETRY) {\n const payload = parsed.payload || {};\n const json = JSON.stringify(payload);\n if (Buffer.byteLength(json, \"utf8\") > 48 * 1024) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"DRONE_TELEMETRY payload too large.\" })));\n return;\n }\n broadcastExcept(createEnvelope(WS_MESSAGE.DRONE_TELEMETRY, { ...payload, relayFrom: socket.clientId }), socket);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_SOCIAL_PULSE) {\n handleCastleSocialPulse(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_WAL_PEER_FEED) {\n handleCastleWalPeerFeed(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.OPEN_DATA_QUERY) {\n queryOpenData(parsed.payload)\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: true, result })));\n })\n .catch((e) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: false, error: e.message })));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_QUERY) {\n runRhizohBrainV2({\n ...(parsed.payload || {}),\n uid: socket?.auth?.user?.uid || \"unknown\"\n })\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, result)));\n })\n .catch(async () => {\n const fallback = await runRhizohBrain(parsed.payload || {}).catch(() => ({\n ok: false,\n reply: \"Rhizoh temporary disturbance. Habitat remains stable.\"\n }));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, fallback)));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.BROADCAST_REGISTER) {\n const role = String(parsed.payload?.role || \"\");\n if (role === \"GENESIS_BROADCAST_AGENT\") {\n broadcasterClientId = socket.clientId;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, registered: role })));\n broadcastState();\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.STUDIO_CUE) {\n // Only registered broadcaster can push global cues.\n if (!broadcasterClientId || socket.clientId !== broadcasterClientId) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Only broadcaster agent can emit STUDIO_CUE.\" })));\n return;\n }\n const cue = parsed.payload || {};\n if (typeof cue !== \"object\") {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid STUDIO_CUE payload.\" })));\n return;\n }\n broadcast(createEnvelope(WS_MESSAGE.STUDIO_CUE, cue));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_JOIN_ROOM) {\n const requested = String(parsed.payload?.roomId || \"\").trim();\n const requestedName = String(parsed.payload?.roomName || \"\").trim();\n const roomId = ensureRoom(requested || `room-${Math.random().toString(36).slice(2, 7)}`, requestedName || null);\n spiralState.activeByClient.set(socket.clientId, roomId);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, roomId })));\n broadcastSpiralState();\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_CREATE_CHARACTER) {\n const name = String(parsed.payload?.name || \"\").trim().slice(0, 32) || `AI-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;\n const role = String(parsed.payload?.role || \"agent\").trim().slice(0, 24) || \"agent\";\n const roomId = ensureRoom(\n String(parsed.payload?.roomId || spiralState.activeByClient.get(socket.clientId) || spiralState.rooms[0].id),\n null\n );\n\n // Character creation always maps to an authoritative world agent.\n const result = orchestrator.applyCommand({\n command: COMMAND.SPAWN_AGENT,\n params: { targetCastleId: \"castle-01\" }\n });\n if (!result?.ok || !result?.data?.id) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Character creation failed.\" })));\n return;\n }\n const id = result.data.id;\n spiralState.characters.set(id, { id, name, role, roomId, source: \"spawned\" });\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, characterId: id, roomId })));\n broadcastSpiralState();\n return;\n }\n });\n\n socket.on(\"close\", () => {\n if (socket.clientId === broadcasterClientId) broadcasterClientId = null;\n clientStats.delete(socket.clientId);\n spiralState.activeByClient.delete(socket.clientId);\n removeCastleSocialClientFromAllRooms(socket.clientId);\n broadcast(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }));\n broadcastState();\n });\n});\n\nsetInterval(() => {\n void (async () => {\n try {\n const { frame, snapshot, delta, packedDelta } = orchestrator.tick(1 / 20);\n syncSpiralCharactersWithWorld(snapshot);\n broadcast(createEnvelope(WS_MESSAGE.NETWORK_TICK, { frame }));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_TICK, snapshot));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA, delta));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA_PACKED, packedDelta));\n broadcastSpiralState();\n await runAcademyEventTick();\n const executed = await tickAutomations(Date.now());\n for (const a of executed) {\n await addTranscript(a.uid, {\n source: \"planner\",\n eventType: \"automation-executed\",\n text: `Automation executed: ${a.title} (${a.action})`,\n roomId: \"studio-main\",\n meta: { automationId: a.id, action: a.action, deviceId: a.deviceId || \"\" }\n });\n }\n } catch (e) {\n console.error(\"[GATEWAY] world tick / automations loop error:\", String(e?.message || e));\n }\n })();\n}, 50);\n\n(async () => {\n await rhizohRuntime.telemetry.initOpenTelemetry();\n if (String(process.env.CASTLE_GENESIS_DISK_PERSIST ?? \"\").trim() === \"\") {\n process.env.CASTLE_GENESIS_DISK_PERSIST = \"1\";\n }\n const genesisDiskBoot = await hydrateGenesisContinuityPersistenceBootV0();\n if (!genesisDiskBoot.ok) {\n console.warn(\"[GATEWAY] genesis disk hydrate failed; continuing with in-memory genesis continuity\");\n }\n httpServer.listen(PORT, () => {\n startGenesisCanonicalClock();\n installGenesisCheckpointSurfaceGetter(buildGenesisRuntimeSurfacePayloadLive);\n setGenesisContinuityAfterPublishHook((seq) => {\n noteGenesisCheckpointSeqCommitted(seq);\n noteGenesisContinuitySeqAcceptedV0(seq);\n });\n startGenesisContinuityInfraSampler(buildGenesisRuntimeSurfacePayloadLive, 2000);\n logProductionObservatorySurfaceGuardsV0();\n logGatewaySubstrateAuthorityGuardsV0();\n console.log(`[GATEWAY] ws/http://localhost:${PORT}`);\n console.log(\n `[GATEWAY] genesis continuity: GET ${rhizohRuntime.routes.genesisRuntime} | POST ${rhizohRuntime.routes.genesisIngress} | SSE ${rhizohRuntime.routes.genesisStream} | replay ${rhizohRuntime.routes.genesisReplay} | diff ${rhizohRuntime.routes.genesisReplayDiff} | equiv ${rhizohRuntime.routes.genesisReplayEquivalence} | analytics ${rhizohRuntime.routes.genesisReplayAnalytics} | evolution ${rhizohRuntime.routes.genesisReplayEvolution}`\n );\n if (String(process.env.CASTLE_GENESIS_OBSERVATORY_LIVE_V01 ?? \"\").trim() === \"1\") {\n console.log(\"GENESIS OBSERVATORY LIVE v0.1 — READ ONLY PHASE STARTED\");\n }\n });\n})().catch((e) => {\n console.error(\"[GATEWAY] fatal boot (before or during listen):\", e);\n process.exit(1);\n});\n"}, "94bcd96e3979326b660fd65084c3945c6764ef9d": {"t": 1785754170.057371, "n": 152828, "h": "4b088345b60cab09", "c": "import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport dotenv from \"dotenv\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconsole.log(\"🔥 [GENESIS_BOOT] entry file:\", __filename);\nconsole.log(\"🔥 [GENESIS_BOOT] node env:\", process.env.NODE_ENV);\nconsole.log(\"🔥 [GENESIS_BOOT] pid:\", process.pid);\nconsole.log(\"🧬 [GENESIS_BOOT] cwd:\", process.cwd());\nconsole.log(\"🧬 [GENESIS_BOOT] import.meta.url:\", import.meta.url);\ndotenv.config({ path: path.join(__dirname, \"..\", \".env\") });\ndotenv.config({ path: path.join(__dirname, \"..\", \".env.local\"), override: true });\n\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport { createServer } from \"node:http\";\nimport { createOrchestrator } from \"@castle/orchestrator\";\nimport { parseTextToCommand } from \"@castle/command-dsl\";\nimport { WS_MESSAGE, COMMAND, createEnvelope, safeJsonParse } from \"@castle/protocol\";\nimport { queryOpenData } from \"./openData.js\";\nimport { verifyClientToken } from \"./auth.js\";\nimport { runRhizohBrain } from \"./rhizohBrain.js\";\nimport { queryRhizohLlm } from \"./rhizohLlmGateway.js\";\nimport { rhizohGatewayTurn } from \"./rhizohGatewayTurn.js\";\nimport {\n RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3,\n runRhizohVoiceTranscribeV3,\n rhizohVoiceTranscribeEnvV3\n} from \"./rhizohVoiceTranscribeV3.js\";\nimport {\n meshAppendDelta,\n meshContinuityAggregate,\n meshJoin,\n meshLeave,\n meshReplayFromNodeId,\n meshReplayFromSeq,\n meshSnapshot,\n meshSubscribeSse\n} from \"./presenceRoomMesh.js\";\nimport { runRhizohBrainV2 } from \"./rhizohBrainV2.js\";\nimport {\n listConnections,\n createConnection,\n updateConnection,\n deleteConnection,\n setDefaultConnection,\n resolveConnection\n} from \"./llmConnectionsStore.js\";\nimport { checkHttpRateLimit, getHttpClientIp } from \"./castleHttpRateLimit.js\";\nimport {\n submitAbuseReportV0,\n listModerationQueueV0,\n softBlockUserV0,\n removeSoftBlockV0,\n listSoftBlockedUsersV0\n} from \"./ops/moderationMvpV0.js\";\nimport { listRecentAgentSnapshotsV0 } from \"./ops/agentObservabilityV0.js\";\nimport { readAgentContainmentConfigV0 } from \"./ops/agentContainmentV0.js\";\nimport { readCostContainmentConfigV0, getCostContainmentStatsV0 } from \"./ops/costContainmentV0.js\";\nimport { getGlobalCostLedgerSnapshotV0, getGclBackendStatusV0 } from \"./ops/globalCostLedgerV0.js\";\nimport { evaluateEconomicStrategyL3V0 } from \"./ops/economicStrategyEngineL3V0.js\";\nimport { getPhasedRolloutStatsV0, readPhasedRolloutTierV0 } from \"./ops/phasedRolloutV0.js\";\nimport { buildUnifiedStateNarrativeV0 } from \"./ops/unifiedStateNarrativeV0.js\";\nimport { resolveHardeningTenantIdV0 } from \"./ops/narrativeTenantIsolationV0.js\";\nimport {\n classifyStressResponseV0,\n verifyStressTaxonomyCoverageV0,\n verifyStressConflictResolutionV0\n} from \"./ops/stressResponseTaxonomyV0.js\";\nimport { verifyResolutionStabilityEnvelopeV0 } from \"./ops/resolutionStabilityEnvelopeV0.js\";\nimport { logLlmAccess } from \"./castleLlmAudit.js\";\nimport { appendMemory, listMemories, getMemoryContext, getPersonaGoalMemory, setPersonaGoalMemory, autoCompactMemories } from \"./memoryStore.js\";\nimport { getFirebasePersistence } from \"./firebasePersistence.js\";\nimport { registerAgentIdentity, listAgentIdentities, getAgentIdentity, updateAgentIdentity } from \"./agentIdentityStore.js\";\nimport { queueAcademyEvent, listAcademyEvents, resolveAcademyEvent, runAcademyEventTick } from \"./academyEventEngine.js\";\nimport { enqueuePdfJob, getPdfJob } from \"./pdfPipeline.js\";\nimport {\n createPublishSession,\n listPublishSessions,\n updatePublishSession,\n addTranscript,\n listTranscripts,\n searchMetadata,\n registerDevice,\n listDevices,\n updateDevice,\n appendTelemetry,\n listTelemetry,\n createAutomation,\n listAutomations,\n tickAutomations,\n enqueueCommand,\n listCommands,\n updateCommandStatus,\n registerSocialChannel,\n listSocialChannels\n} from \"./studioOpsStore.js\";\nimport { metrics as infraMetrics } from \"./infra/metrics.js\";\nimport { renderRhizohPrometheusMetrics, rhizohEnterpriseMetrics } from \"./infra/rhizohEnterpriseMetrics.js\";\nimport { scoreHealth } from \"./infra/healthScore.js\";\nimport { getTraceById, listTracesBySession } from \"./infra/traceRegistry.js\";\nimport { canonicalEpistemicSealString, hashAndSignEpistemicSeal, EPISTEMIC_SEAL_SCHEMA } from \"./epistemicSeal.js\";\nimport { persistEpistemicLedgerBatch } from \"./epistemicLedgerStore.js\";\nimport { persistEpistemicForecastBatch } from \"./epistemicForecastStore.js\";\nimport { buildRhizohExternalGroundTruthPayload } from \"./rhizohExternalGroundTruthGateway.js\";\nimport { ingestRhizohExternalLossBatchHttp } from \"./rhizohExternalLossIngestGateway.js\";\nimport {\n ingestRhizohProductOutcomeHttp,\n listRhizohProductOutcomeAggregates,\n verifyRhizohOutcomeHmac,\n verifyRhizohOutcomeSourceToken\n} from \"./rhizohProductOutcomeIngestGateway.js\";\nimport { initRhizoh } from \"./rhizohProductionBootstrap.js\";\nimport {\n buildGenesisRuntimeSurfacePayload,\n recordGenesisEpistemicLedgerPersisted,\n recordGenesisEpistemicSealIssued,\n startGenesisCanonicalClock\n} from \"./genesisRuntimeSurfaceV0.js\";\nimport { registerGenesisSseClient, getGenesisContinuitySeq, setGenesisContinuityAfterPublishHook } from \"./genesisContinuityStreamHubV0.js\";\nimport { computeGenesisReplayFingerprintV0 } from \"./genesisReplayFingerprintV0.js\";\nimport { buildGenesisGatewayCapabilitiesV0 } from \"./genesisGatewayCapabilitiesV0.js\";\nimport {\n installGenesisCheckpointSurfaceGetter,\n noteGenesisCheckpointSeqCommitted,\n getLatestGenesisSignedCheckpoint\n} from \"./genesisContinuityCheckpointV0.js\";\nimport { hydrateGenesisContinuityPersistenceBootV0 } from \"./genesisContinuityHydrateBootV0.js\";\nimport { startGenesisContinuityInfraSampler } from \"./genesisContinuityInfraSamplerV0.js\";\nimport {\n genesisCheckpointQueryBySeqV0,\n genesisCheckpointQueryRangeV0,\n genesisCheckpointQueryLineageV0\n} from \"./genesisCheckpointQueryV0.js\";\nimport {\n genesisContinuityEventArchiveEnabled,\n queryGenesisContinuityEventArchiveV0,\n GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION\n} from \"./genesisContinuityEventArchiveV0.js\";\nimport { resolveGenesisReplayRouterV1, GENESIS_REPLAY_ROUTER_SCHEMA } from \"./genesisReplayRouterV1.js\";\nimport {\n computeGenesisReplayTemporalDiffV1,\n GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA\n} from \"./genesisReplayTemporalDiffV1.js\";\nimport { compareGenesisReplayEquivalenceV1 } from \"./genesisReplayEquivalenceV1.js\";\nimport { computeGenesisReplayAnalyticsV1 } from \"./genesisReplayAnalyticsV1.js\";\nimport { computeGenesisReplayEvolutionV1 } from \"./genesisReplayEvolutionV1.js\";\nimport { handleStripeCheckoutCreate, handleStripeMembershipWebhook } from \"./stripeWebhookMembership.js\";\nimport { buildRealityHealthPayload } from \"./edgeRealityConsistencyV1.js\";\nimport {\n assertWalPeerSocketAuthorityV0,\n buildGatewaySubstrateAuthoritySnapshotV0,\n logGatewaySubstrateAuthorityGuardsV0,\n validateWalPeerFeedSignatureV0\n} from \"./gatewaySubstrateAuthorityV0.js\";\nimport {\n buildSubstrateOperationalSnapshotV0,\n recordWalPeerRejectV0,\n renderSubstratePrometheusMetricsV0\n} from \"./infra/substrateOperationalMetrics.js\";\nimport { ingestClientSubstrateHealthV0 } from \"./rhizohSubstrateHealthIngestV0.js\";\nimport { recordSubstrateOtelEventV0 } from \"./infra/opentelemetrySubstrateV0.js\";\nimport {\n GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n getGenesisWorldObservationIngressStatusV0,\n ingestGenesisWorldObservationV0\n} from \"./genesisContinuityClientIngressV0.js\";\nimport {\n getGenesisContinuitySeqAuditV0,\n noteGenesisContinuitySeqAcceptedV0\n} from \"./genesisContinuitySeqGapV0.js\";\n\n/** Render / Fly / Railway: `PORT` — yoksa `CASTLE_GATEWAY_PORT` — yoksa 8090. */\nconst PORT =\n Number(process.env.PORT) ||\n Number(process.env.CASTLE_GATEWAY_PORT) ||\n 8090;\n/** Spiral / world WebSocket server — set after `wss` init; used by genesis runtime surface. */\nlet spiralWssForGenesis = null;\nconst MAX_MESSAGE_BYTES = Number(process.env.CASTLE_MAX_MESSAGE_BYTES || 32 * 1024);\nconst REQUIRED_GATEWAY_TOKEN = process.env.CASTLE_GATEWAY_TOKEN || \"\";\nconst ALLOWED_ORIGINS = (process.env.CASTLE_ALLOWED_ORIGINS || \"\")\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n\nfunction normalizeHttpOrigin(origin) {\n return String(origin || \"\")\n .trim()\n .replace(/\\/+$/, \"\");\n}\n\n/** HTTP CORS: `CASTLE_ALLOWED_ORIGINS` + `CASTLE_HTTP_CORS_ORIGIN` (tek origin); `CASTLE_HTTP_CORS_ORIGIN=*` → wildcard. */\nfunction buildHttpCorsOriginAllowSet() {\n const set = new Set();\n for (const o of ALLOWED_ORIGINS) {\n const n = normalizeHttpOrigin(o);\n if (n) set.add(n);\n }\n const primary = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n if (primary && primary !== \"*\") set.add(primary);\n return set;\n}\n\nconst HTTP_CORS_ORIGIN_ALLOW_SET = buildHttpCorsOriginAllowSet();\n\n/**\n * Tarayıcı `Origin` ile tam eşleşme ister. Tek sabit `CASTLE_HTTP_CORS_ORIGIN` yeterli değil:\n * örn. `web.app` vs `firebaseapp.com` veya Render’da yanlış/eksik env.\n * @returns {string|null} `Access-Control-Allow-Origin` değeri; null = başlık yok (istemci CORS fail).\n */\nfunction accessControlAllowOriginValue(req) {\n const primaryEnv = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n if (primaryEnv === \"*\") return \"*\";\n const reqOrigin = normalizeHttpOrigin(req.headers?.origin);\n if (HTTP_CORS_ORIGIN_ALLOW_SET.size === 0) return \"*\";\n if (!reqOrigin) return \"*\";\n if (HTTP_CORS_ORIGIN_ALLOW_SET.has(reqOrigin)) return reqOrigin;\n return null;\n}\n\nfunction applyHttpCorsHeaders(req, res) {\n const allow = accessControlAllowOriginValue(req);\n if (allow) {\n res.setHeader(\"Access-Control-Allow-Origin\", allow);\n if (allow !== \"*\") res.setHeader(\"Vary\", \"Origin\");\n }\n res.setHeader(\"Access-Control-Allow-Methods\", \"GET,POST,OPTIONS\");\n res.setHeader(\n \"Access-Control-Allow-Headers\",\n \"Content-Type, Authorization, X-Castle-Dev-Uid, X-Castle-Guest-Id, X-Castle-Gateway-Token, X-Rhizoh-Outcome-Signature, X-Rhizoh-Outcome-Source-Token\"\n );\n}\nconst REQUIRE_AUTH = process.env.CASTLE_REQUIRE_AUTH === \"true\";\nconst ALLOW_DEV_ANON = process.env.CASTLE_ALLOW_DEV_ANON !== \"false\";\nconst ALLOW_DEV_HTTP_UID = process.env.CASTLE_ALLOW_DEV_HTTP_UID !== \"false\";\n/** Presence mesh: allow `X-Castle-Guest-Id` when Firebase auth absent (default on). Set `false` to require auth only. */\nconst ALLOW_MESH_GUEST = process.env.CASTLE_ALLOW_MESH_GUEST !== \"false\";\nconst SOCIAL_RETRY_MAX = Math.max(0, Math.min(5, Number(process.env.CASTLE_SOCIAL_RETRY_MAX || 2)));\nconst SOCIAL_RETRY_BASE_MS = Math.max(100, Number(process.env.CASTLE_SOCIAL_RETRY_BASE_MS || 700));\nconst TELEGRAM_BOT_TOKEN = process.env.CASTLE_TELEGRAM_BOT_TOKEN || \"\";\nconst WHATSAPP_TOKEN = process.env.CASTLE_WHATSAPP_TOKEN || \"\";\nconst WHATSAPP_PHONE_NUMBER_ID = process.env.CASTLE_WHATSAPP_PHONE_NUMBER_ID || \"\";\nconst RL_RHIZOH_LLM_PER_MIN = Math.max(5, Number(process.env.CASTLE_RL_RHIZOH_LLM_PER_MIN || 40));\nconst RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN || 30)\n);\nconst RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN || 120));\nconst RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN = Math.max(8, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN || 48));\nconst RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN = Math.max(4, Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN || 24));\nconst RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN || 18)\n);\nconst RL_EPISTEMIC_SEAL_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_EPISTEMIC_SEAL_PER_MIN || 120));\nconst RL_EPISTEMIC_LOG_PER_MIN = Math.max(20, Number(process.env.CASTLE_RL_EPISTEMIC_LOG_PER_MIN || 240));\nconst EPISTEMIC_SEAL_SECRET = String(\n process.env.CASTLE_EPISTEMIC_SEAL_SECRET || process.env.CASTLE_GATEWAY_TOKEN || \"\"\n).trim();\nconst RL_LLM_CONN_TEST_PER_MIN = Math.max(2, Number(process.env.CASTLE_RL_LLM_CONNECTION_TEST_PER_MIN || 8));\n\n/** One-shot production warnings: CORS surface, auth toggle, dev bypass flags (observatory deploy hygiene). */\nfunction logProductionObservatorySurfaceGuardsV0() {\n if (String(process.env.NODE_ENV || \"\").toLowerCase() !== \"production\") return;\n const tag = \"[GATEWAY_PROD_GUARD]\";\n const primaryCors = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n const corsListEmpty = ALLOWED_ORIGINS.length === 0;\n if (corsListEmpty && !primaryCors) {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty and CASTLE_HTTP_CORS_ORIGIN empty — browser SSE/replay may fail; set explicit Firebase Hosting origins`\n );\n } else if (corsListEmpty && primaryCors && primaryCors !== \"*\") {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty — only CASTLE_HTTP_CORS_ORIGIN set; add web.app + firebaseapp.com (and previews) to CASTLE_ALLOWED_ORIGINS if clients use alternate hostnames`\n );\n }\n if (primaryCors && /localhost|127\\.0\\.0\\.1/i.test(primaryCors)) {\n console.warn(`${tag} CORS: CASTLE_HTTP_CORS_ORIGIN is localhost while NODE_ENV=production`);\n }\n if (REQUIRE_AUTH) {\n console.warn(\n `${tag} AUTH: CASTLE_REQUIRE_AUTH=true — clients must send a valid gateway credential (otherwise silent 401 on replay/SSE); EventSource cannot set custom headers unless proxied`\n );\n if (REQUIRED_GATEWAY_TOKEN.length < 16) {\n console.warn(`${tag} AUTH: CASTLE_GATEWAY_TOKEN missing or short while require-auth enabled`);\n }\n }\n if (ALLOW_DEV_ANON) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_ANON is not \"false\" — dev anonymous path may be live in production (set CASTLE_ALLOW_DEV_ANON=false)`\n );\n }\n if (ALLOW_DEV_HTTP_UID) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_HTTP_UID is not \"false\" — X-Castle-Dev-Uid bypass risk (set CASTLE_ALLOW_DEV_HTTP_UID=false)`\n );\n }\n}\n\nconst rhizohRuntime = initRhizoh();\n\n/** İstemci: llmKeySource veya keySource — env | user_connection | auto */\nfunction normalizeClientLlmKeySource(raw) {\n const s = String(raw ?? \"\").trim().toLowerCase();\n if (s === \"env\" || s === \"server\") return \"env\";\n if (s === \"user_connection\" || s === \"connection\" || s === \"user\") return \"user_connection\";\n if (s === \"auto\" || s === \"\") return \"auto\";\n return null;\n}\n\nfunction sendJson(res, statusCode, payload) {\n res.writeHead(statusCode, { \"Content-Type\": \"application/json; charset=utf-8\" });\n res.end(JSON.stringify(payload));\n}\n\nfunction logRhizohHealth(stage, detail = {}) {\n try {\n console.info(`[CASTLE_${String(stage || \"unknown\")}]`, detail && typeof detail === \"object\" ? detail : {});\n } catch {\n /* noop */\n }\n}\n\nfunction renderInfraMetricsProm() {\n const lines = [\n `castle_gateway_events_processed_total ${infraMetrics.eventsProcessed}`,\n `castle_gateway_duplicate_reject_total ${infraMetrics.duplicateRejects}`,\n `castle_gateway_enqueue_latency_ms ${infraMetrics.enqueueLatencyMs}`,\n `castle_gateway_queue_depth ${infraMetrics.queueDepth}`,\n `castle_gateway_queue_lag_seconds ${(Number(infraMetrics.queueLag || 0) / 1000).toFixed(3)}`,\n `castle_gateway_errors_total ${infraMetrics.errors}`\n ];\n const buckets = infraMetrics.enqueueLatencyBuckets || {};\n for (const k of Object.keys(buckets)) {\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"${k}\"} ${buckets[k]}`);\n }\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"+Inf\"} ${infraMetrics.eventsProcessed}`);\n if (process.env.CASTLE_SUBSTRATE_METRICS !== \"0\") {\n lines.push(renderSubstratePrometheusMetricsV0().trimEnd());\n }\n return lines.join(\"\\n\");\n}\n\nasync function fetchJsonWithTimeout(url, timeoutMs = 1200) {\n const ctl = new AbortController();\n const tid = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const res = await fetch(url, { signal: ctl.signal });\n if (!res.ok) return null;\n return await res.json();\n } catch {\n return null;\n } finally {\n clearTimeout(tid);\n }\n}\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction getHttpPathname(req) {\n let p = String(req.url || \"\").split(\"?\")[0];\n /* Reverse proxies / clients sometimes append `/` — avoid silent 404 on e.g. `/rhizoh/genesis/stream/`. */\n while (p.length > 1 && p.endsWith(\"/\")) {\n p = p.slice(0, -1);\n }\n return p;\n}\n\nfunction rhizohLlmEnvConfigured() {\n const p = String(process.env.CASTLE_LLM_PROVIDER || \"openai\").toLowerCase();\n const key =\n p === \"anthropic\"\n ? process.env.ANTHROPIC_API_KEY\n : p === \"gemini\"\n ? process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY\n : p === \"xai\"\n ? process.env.XAI_API_KEY\n : p === \"deepseek\"\n ? process.env.DEEPSEEK_API_KEY\n : p === \"mistral\"\n ? process.env.MISTRAL_API_KEY\n : p === \"openrouter\"\n ? process.env.OPENROUTER_API_KEY\n : process.env.OPENAI_API_KEY;\n return !!String(key || \"\").trim();\n}\n\n/**\n * @param {import(\"firebase-admin/firestore\").Firestore | null} db\n * @param {number} timeoutMs\n */\nasync function probeFirestoreReachable(db, timeoutMs = 2000) {\n if (!db) return { ok: false, reason: \"no_db\" };\n const timeout = sleep(timeoutMs).then(() => ({ ok: false, reason: \"timeout\" }));\n const work = db\n .collection(\"_castle_gateway_health\")\n .doc(\"ping\")\n .get()\n .then(() => ({ ok: true, reason: \"reachable\" }))\n .catch((e) => ({ ok: false, reason: String(e?.message || e || \"firestore_error\") }));\n return Promise.race([work, timeout]);\n}\n\nfunction stripHtml(text = \"\") {\n return String(text)\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \" \")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \" \")\n .replace(/<[^>]+>/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction haversineMeters(aLat, aLon, bLat, bLon) {\n const R = 6371000;\n const dLat = ((bLat - aLat) * Math.PI) / 180;\n const dLon = ((bLon - aLon) * Math.PI) / 180;\n const sa = Math.sin(dLat / 2);\n const sb = Math.sin(dLon / 2);\n const x = sa * sa + Math.cos((aLat * Math.PI) / 180) * Math.cos((bLat * Math.PI) / 180) * sb * sb;\n return 2 * R * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));\n}\n\nasync function checkPublishTargetHealth(protocol, target) {\n const p = String(protocol || \"\").toUpperCase();\n const t = String(target || \"\").trim();\n if (!t) return { ok: false, reason: \"target_missing\", shouldReconnect: false };\n if (p === \"RTMP\") {\n const ok = t.startsWith(\"rtmp://\") || t.startsWith(\"rtmps://\");\n return { ok, reason: ok ? \"rtmp_target_format_ok\" : \"invalid_rtmp_target\", shouldReconnect: !ok, reconnectInSec: 15 };\n }\n if (p === \"WHIP\" || p === \"SFU\") {\n try {\n const res = await fetch(t, { method: \"OPTIONS\" });\n const ok = res.status < 500;\n return { ok, reason: `http_${res.status}`, shouldReconnect: !ok, reconnectInSec: 8 };\n } catch {\n return { ok: false, reason: \"network_unreachable\", shouldReconnect: true, reconnectInSec: 8 };\n }\n }\n return { ok: true, reason: \"unchecked_protocol\", shouldReconnect: false };\n}\n\nasync function fetchTextFromUrl(url) {\n const res = await fetch(url, { method: \"GET\" });\n if (!res.ok) throw new Error(`fetch_${res.status}`);\n const ct = String(res.headers.get(\"content-type\") || \"\");\n if (ct.includes(\"application/pdf\")) throw new Error(\"pdf_binary_not_supported_directly\");\n const body = await res.text();\n return stripHtml(body).slice(0, 14000);\n}\n\nfunction buildTaskActionGraph(task = \"\", context = {}) {\n const t = String(task || \"\").toLowerCase();\n if (t.includes(\"dinner\") || t.includes(\"akşam yemeği\") || t.includes(\"aksam yemegi\")) {\n return {\n taskTitle: \"Dinner Prep Planner\",\n nodes: [\n { id: \"n1\", title: \"Kitchen preheat\", action: \"smart-oven.preheat\", params: { temperatureC: 190, mode: \"fan\" } },\n { id: \"n2\", title: \"Lighting comfort\", action: \"smart-light.set_scene\", params: { scene: \"warm_dinner\" } },\n { id: \"n3\", title: \"Fridge inventory check\", action: \"smart-fridge.inventory_scan\", params: {} },\n { id: \"n4\", title: \"Reminder notification\", action: \"smart-watch.notify\", params: { text: \"Dinner prep started.\" } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" },\n { from: \"n3\", to: \"n4\" }\n ]\n };\n }\n return {\n taskTitle: task || \"General Planner\",\n nodes: [\n { id: \"n1\", title: \"Safety check\", action: \"device.safety_check\", params: { context } },\n { id: \"n2\", title: \"Primary action\", action: \"device.execute_task\", params: { task } },\n { id: \"n3\", title: \"Report\", action: \"studio.timeline_note\", params: { task } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" }\n ]\n };\n}\n\nfunction buildEthicsPrograms() {\n return {\n policyVersion: \"v1\",\n tracks: [\n {\n id: \"elderly-care-assist\",\n audience: \"elderly\",\n title: \"Elderly Assistance Track\",\n principles: [\"human-in-the-loop\", \"consent-first\", \"low-friction alerts\"],\n modules: [\"medication-reminder\", \"fall-risk-check\", \"family-notify\", \"daily-companion-brief\"]\n },\n {\n id: \"kids-safe-learning\",\n audience: \"children\",\n title: \"Kids Safe Learning Track\",\n principles: [\"guardian-consent\", \"age-appropriate-language\", \"content-filtering\"],\n modules: [\"study-coach\", \"screen-time-balance\", \"safe-social-share\", \"emergency-contact\"]\n }\n ]\n };\n}\n\nasync function deliverViaTelegram(channel, text) {\n const chatId = String(channel.endpoint || \"\").trim();\n if (!TELEGRAM_BOT_TOKEN) throw new Error(\"telegram_token_missing\");\n if (!chatId) throw new Error(\"telegram_chat_id_missing\");\n const url = `https://api.telegram.org/bot${encodeURIComponent(TELEGRAM_BOT_TOKEN)}/sendMessage`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ chat_id: chatId, text })\n });\n if (!res.ok) throw new Error(`telegram_http_${res.status}`);\n return { provider: \"telegram\", status: res.status };\n}\n\nasync function deliverViaWhatsapp(channel, text) {\n const to = String(channel.endpoint || \"\").trim();\n if (!WHATSAPP_TOKEN) throw new Error(\"whatsapp_token_missing\");\n if (!WHATSAPP_PHONE_NUMBER_ID) throw new Error(\"whatsapp_phone_number_id_missing\");\n if (!to) throw new Error(\"whatsapp_to_missing\");\n const url = `https://graph.facebook.com/v22.0/${encodeURIComponent(WHATSAPP_PHONE_NUMBER_ID)}/messages`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${WHATSAPP_TOKEN}` },\n body: JSON.stringify({ messaging_product: \"whatsapp\", to, type: \"text\", text: { body: text } })\n });\n if (!res.ok) throw new Error(`whatsapp_http_${res.status}`);\n return { provider: \"whatsapp\", status: res.status };\n}\n\nasync function deliverViaWebhook(channel, text) {\n const endpoint = String(channel.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"webhook_endpoint_missing\");\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text, platform: channel.platform || \"webhook\", ts: Date.now() })\n });\n if (!res.ok) throw new Error(`webhook_http_${res.status}`);\n return { provider: \"webhook\", status: res.status };\n}\n\nasync function deliverSocialMessage(channel, text) {\n const platform = String(channel.platform || \"webhook\").toLowerCase();\n if (platform === \"telegram\") return deliverViaTelegram(channel, text);\n if (platform === \"whatsapp\") return deliverViaWhatsapp(channel, text);\n return deliverViaWebhook(channel, text);\n}\n\nasync function deliverSocialWithRetry(channel, text) {\n let attempt = 0;\n let lastError = null;\n while (attempt <= SOCIAL_RETRY_MAX) {\n try {\n const result = await deliverSocialMessage(channel, text);\n return { ok: true, attempt: attempt + 1, ...result };\n } catch (error) {\n lastError = error;\n if (attempt >= SOCIAL_RETRY_MAX) break;\n const jitter = Math.floor(Math.random() * 90);\n await sleep(SOCIAL_RETRY_BASE_MS * Math.pow(2, attempt) + jitter);\n }\n attempt += 1;\n }\n return { ok: false, attempt: attempt + 1, error: String(lastError?.message || \"delivery_failed\") };\n}\n\nasync function dispatchCommandToAdapter(device, command) {\n const adapter = String(command?.adapter || device?.adapter || \"websocket\").toLowerCase();\n const endpoint = String(device?.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"device_endpoint_missing\");\n if (adapter === \"mqtt\") {\n // MQTT delivery is proxied via a bridge endpoint (HTTP webhook style).\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n type: \"mqtt-command\",\n topic: `castle/devices/${device.id}/commands`,\n action: command.action,\n params: command.params || {}\n })\n });\n if (!res.ok) throw new Error(`mqtt_bridge_http_${res.status}`);\n return { ok: true, adapter, via: \"http-bridge\", status: res.status };\n }\n if (!endpoint.startsWith(\"ws://\") && !endpoint.startsWith(\"wss://\")) throw new Error(\"invalid_websocket_endpoint\");\n const sendResult = await new Promise((resolve, reject) => {\n const ws = new WebSocket(endpoint);\n const timer = setTimeout(() => {\n try {\n ws.close();\n } catch {\n /* noop */\n }\n reject(new Error(\"websocket_timeout\"));\n }, 3000);\n ws.on(\"open\", () => {\n ws.send(\n JSON.stringify({\n type: \"castle-command\",\n commandId: command.id,\n deviceId: command.deviceId,\n action: command.action,\n params: command.params || {}\n }),\n (err) => {\n clearTimeout(timer);\n ws.close();\n if (err) reject(new Error(\"websocket_send_failed\"));\n else resolve({ ok: true, adapter: \"websocket\", via: \"ws\" });\n }\n );\n });\n ws.on(\"error\", () => {\n clearTimeout(timer);\n reject(new Error(\"websocket_connect_failed\"));\n });\n });\n return sendResult;\n}\n\nfunction readHttpJson(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => {\n if (!buf) return resolve({});\n const payload = safeJsonParse(buf);\n if (!payload || typeof payload !== \"object\") return reject(new Error(\"invalid_json_payload\"));\n resolve(payload);\n });\n req.on(\"error\", reject);\n });\n}\n\n/** Ham gövde (HMAC doğrulaması için). */\nfunction readHttpBodyText(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => resolve(buf));\n req.on(\"error\", reject);\n });\n}\n\nfunction maskOpaqueId(value) {\n const t = String(value || \"\");\n if (!t) return \"(empty)\";\n if (t.length <= 8) return \"****\";\n return `${t.slice(0, 4)}…${t.slice(-2)}`;\n}\n\nfunction readGatewayHttpToken(req) {\n const x = String(req.headers?.[\"x-castle-gateway-token\"] || \"\").trim();\n if (x) return x;\n const auth = String(req.headers?.authorization || \"\");\n const m = /^Bearer\\s+(.+)$/i.exec(auth);\n return m ? String(m[1] || \"\").trim() : \"\";\n}\n\nasync function resolveHttpUser(req) {\n const authResult = await verifyClientToken(req);\n if (authResult?.ok && authResult.user?.uid) {\n return { ok: true, uid: String(authResult.user.uid), mode: authResult.kind || \"auth\" };\n }\n if (ALLOW_DEV_ANON && ALLOW_DEV_HTTP_UID) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) return { ok: true, uid: `dev-${devUid.slice(0, 64)}`, mode: \"dev-header\" };\n }\n return { ok: false, reason: authResult?.reason || \"auth_required\" };\n}\n\n/** Mesh actor: Firebase uid, dev header, or `guest:<uuid>` via header / `guestId` query (SSE cannot set headers). */\nasync function resolveMeshActor(req) {\n const auth = await resolveHttpUser(req);\n if (auth.ok) return { ok: true, clientUid: auth.uid, mode: auth.mode };\n if (ALLOW_MESH_GUEST) {\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const gid = String(req.headers?.[\"x-castle-guest-id\"] || q.searchParams.get(\"guestId\") || \"\").trim();\n if (gid) {\n const normalized = gid.startsWith(\"guest:\") ? gid.slice(0, 180) : `guest:${gid.slice(0, 160)}`;\n return { ok: true, clientUid: normalized, mode: \"guest-header\" };\n }\n }\n return { ok: false, reason: \"mesh_auth_required\" };\n}\n\n/** Live genesis runtime JSON (GET /rhizoh/genesis/runtime + SSE infra sampler). */\nasync function buildGenesisRuntimeSurfacePayloadLive() {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n let spiralWsActive = 0;\n if (spiralWssForGenesis) {\n for (const c of spiralWssForGenesis.clients) {\n if (c.readyState === 1) spiralWsActive += 1;\n }\n }\n const base = buildGenesisRuntimeSurfacePayload({\n infraMetrics,\n scoredHealth: scored,\n rhizohEnterpriseMetrics,\n mesh: meshContinuityAggregate(),\n workerHealth: worker,\n port: PORT,\n spiralWebSocketClientsActive: spiralWsActive\n });\n return {\n ...base,\n replayFingerprint: computeGenesisReplayFingerprintV0(base),\n gatewayCapabilities: buildGenesisGatewayCapabilitiesV0(),\n genesisStream: {\n schema: \"castle.genesis.stream_cursor.v0\",\n lastAcceptedSeq: getGenesisContinuitySeq(),\n clientIngress: {\n schema: GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n contract: \"castle.world_observation.ingress_envelope.v1\",\n postPath: rhizohRuntime.routes.genesisIngress,\n closure: getGenesisWorldObservationIngressStatusV0()\n },\n seqContinuity: getGenesisContinuitySeqAuditV0(),\n eventArchive: genesisContinuityEventArchiveEnabled()\n ? {\n enabled: true,\n projection: GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION,\n queryPath: rhizohRuntime.routes.genesisContinuityEvents\n }\n : { enabled: false },\n replayRouter: {\n schema: GENESIS_REPLAY_ROUTER_SCHEMA,\n queryPath: rhizohRuntime.routes.genesisReplay,\n queryHint: \"from=&to= or range=from-to; optional type=&checkpoints=0\",\n temporalDiffPath: rhizohRuntime.routes.genesisReplayDiff,\n temporalDiffSchema: GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA,\n equivalencePath: rhizohRuntime.routes.genesisReplayEquivalence,\n analyticsPath: rhizohRuntime.routes.genesisReplayAnalytics,\n evolutionPath: rhizohRuntime.routes.genesisReplayEvolution\n }\n }\n };\n}\n\nconst httpServer = createServer(async (req, res) => {\n applyHttpCorsHeaders(req, res);\n if (req.method === \"OPTIONS\") {\n res.writeHead(204);\n res.end();\n return;\n }\n\n const pathname = getHttpPathname(req);\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/genesis/__ping\") {\n sendJson(res, 200, {\n ok: true,\n pid: process.pid,\n entry: __filename,\n time: Date.now(),\n genesis: true\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/webhooks/stripe\") {\n await handleStripeMembershipWebhook(req, res);\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/billing/stripe/checkout\") {\n await handleStripeCheckoutCreate(req, res, { readHttpJson, resolveHttpUser, sendJson });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/live\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, live: false, dns: true, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n sendJson(res, 200, { ok: true, live: true, dns: true, service: \"castle-gateway\", ts: Date.now() });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/ready\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, ready: false, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n try {\n const persistence = getFirebasePersistence();\n const llmProbe = rhizohLlmEnvConfigured();\n sendJson(res, 200, {\n ok: true,\n ready: true,\n dns: true,\n persistence: persistence.mode,\n service: \"castle-gateway\",\n gateway: \"ok\",\n llm: llmProbe ? \"ok\" : \"degraded\",\n spine: llmProbe ? \"ok\" : \"partial\"\n });\n } catch (e) {\n sendJson(res, 503, { ok: false, ready: false, reason: String(e?.message || e), service: \"castle-gateway\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/reality\") {\n const payload = buildRealityHealthPayload();\n sendJson(res, payload.ok ? 200 : 503, payload);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/substrate\") {\n const authority = buildGatewaySubstrateAuthoritySnapshotV0();\n const operational = buildSubstrateOperationalSnapshotV0();\n const drift = buildRealityHealthPayload();\n const ok = Boolean(authority.productionAuthoritySatisfied) && drift.ok;\n sendJson(res, ok ? 200 : 503, {\n ok,\n service: \"castle-gateway\",\n ts: Date.now(),\n authority,\n operational,\n drift: { ok: drift.ok, errors: drift.drift?.errors, warnings: drift.drift?.warnings }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/substrate/health\") {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const body = await readHttpJson(req);\n const uid = (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestClientSubstrateHealthV0(body, { uid, clientId: uid });\n if (!result.ok) {\n sendJson(res, 400, result);\n return;\n }\n recordSubstrateOtelEventV0(\"client_health_ingest\", { uid: String(uid).slice(0, 32) });\n sendJson(res, 200, result);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/deps\") {\n const t0 = Date.now();\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, {\n ok: false,\n dns: true,\n llm: false,\n firestore: false,\n phase: \"maintenance\",\n latencyMs: Date.now() - t0,\n service: \"castle-gateway\"\n });\n return;\n }\n const persistence = getFirebasePersistence();\n let firestoreOk = persistence.mode !== \"firebase\";\n let firestoreDetail = persistence.mode === \"file\" ? \"file_mode\" : \"not_applicable\";\n if (persistence.mode === \"firebase\" && persistence.db) {\n const pr = await probeFirestoreReachable(persistence.db, 2000);\n firestoreOk = pr.ok;\n firestoreDetail = pr.reason || (pr.ok ? \"reachable\" : \"fail\");\n } else if (persistence.mode === \"firebase\" && !persistence.db) {\n firestoreOk = false;\n firestoreDetail = \"firebase_not_initialized\";\n }\n const llmOk = rhizohLlmEnvConfigured();\n const overallOk = llmOk && firestoreOk;\n const latencyMs = Date.now() - t0;\n sendJson(res, 200, {\n ok: overallOk,\n dns: true,\n llm: llmOk,\n firestore: persistence.mode === \"firebase\" ? firestoreOk : false,\n persistence: persistence.mode,\n firestoreDetail,\n latencyMs,\n service: \"castle-gateway\",\n wsPort: PORT\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health\") {\n const persistence = getFirebasePersistence();\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n wsPort: PORT,\n persistence: persistence.mode,\n presenceMesh: \"sse\"\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/health\") {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n sendJson(res, 200, {\n ok: true,\n role: \"gateway\",\n queueLagMs: infraMetrics.queueLag,\n queueDepth: infraMetrics.queueDepth,\n errors: infraMetrics.errors,\n status: scored.status,\n score: scored.score,\n reasons: scored.reasons,\n worker: worker\n ? {\n status: worker.status,\n score: worker.score,\n reasons: worker.reasons\n }\n : null,\n timestamp: Date.now()\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisRuntime) {\n const payload = await buildGenesisRuntimeSurfacePayloadLive();\n sendJson(res, 200, payload);\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.genesisIngress) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const actor = await resolveMeshActor(req);\n const clientId = actor.ok ? actor.clientUid : (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestGenesisWorldObservationV0(body, { clientId });\n if (!result.ok) {\n const status =\n result.error === \"rate_limited\" || result.error === \"agent_spoke_throttled\"\n ? 429\n : result.error === \"payload_too_large\"\n ? 413\n : 400;\n sendJson(res, status, result);\n return;\n }\n sendJson(res, result.idempotent ? 200 : 202, result);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisStream) {\n const corsOrigin = accessControlAllowOriginValue(req);\n if (!corsOrigin && String(process.env.NODE_ENV || \"\").toLowerCase() === \"production\") {\n const og = String(req.headers?.origin || \"\").trim();\n if (og) {\n console.warn(\"[GATEWAY] genesis SSE: missing Access-Control-Allow-Origin for browser Origin (CORS whitelist mismatch):\", og);\n }\n }\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n res.write(\"retry: 3000\\n\\n\");\n res.write(\": genesis continuity stream\\n\\n\");\n res.write(\"event: genesis\\n\");\n res.write(`data: {\"booted\":true,\"pid\":${process.pid}}\\n\\n`);\n registerGenesisSseClient(res, req);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLatest) {\n const cp = getLatestGenesisSignedCheckpoint();\n if (!cp) {\n sendJson(res, 404, { ok: false, error: \"no_checkpoint_yet\", hint: \"wait_for_seq_interval_or_configure_signing_secret\" });\n return;\n }\n sendJson(res, 200, { ok: true, checkpoint: cp });\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(rhizohRuntime.routes.genesisCheckpointBySeqPrefix)) {\n const tail = pathname.slice(rhizohRuntime.routes.genesisCheckpointBySeqPrefix.length).split(\"/\")[0];\n const seq = parseInt(String(tail || \"\").trim(), 10);\n const out = await genesisCheckpointQueryBySeqV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"checkpoint_not_found\") {\n sendJson(res, 404, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointRange) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryRangeV0(from, to);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"range_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLineage) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const seq = parseInt(String(u.searchParams.get(\"seq\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryLineageV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"lineage_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisContinuityEvents) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const limit = parseInt(String(u.searchParams.get(\"limit\") || \"\").trim(), 10);\n const out = await queryGenesisContinuityEventArchiveV0(from, to, type, limit);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\" || out.error === \"event_archive_disabled\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"archive_file_too_large\") {\n sendJson(res, 413, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplay) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await resolveGenesisReplayRouterV1({\n from: fromSeq,\n to: toSeq,\n type,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayDiff) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const out = await computeGenesisReplayTemporalDiffV1({ from: fromSeq, to: toSeq, type });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEquivalence) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from1 = parseInt(String(u.searchParams.get(\"from1\") || \"\").trim(), 10);\n const to1 = parseInt(String(u.searchParams.get(\"to1\") || \"\").trim(), 10);\n const from2 = parseInt(String(u.searchParams.get(\"from2\") || \"\").trim(), 10);\n const to2 = parseInt(String(u.searchParams.get(\"to2\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await compareGenesisReplayEquivalenceV1({\n from1,\n to1,\n from2,\n to2,\n type,\n includeCheckpoints\n });\n if (\n !out.ok &&\n (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")\n ) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayAnalytics) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayAnalyticsV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEvolution) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const collapseWindows = parseInt(String(u.searchParams.get(\"collapseWindows\") || \"0\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayEvolutionV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n collapseWindows,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/metrics\") {\n res.writeHead(200, { \"Content-Type\": \"text/plain; version=0.0.4; charset=utf-8\" });\n res.end(renderInfraMetricsProm());\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.infraPrometheusMetrics) {\n if (process.env.CASTLE_PROMETHEUS_METRICS !== \"1\") {\n sendJson(res, 404, { ok: false, error: \"prometheus_metrics_disabled\" });\n return;\n }\n res.writeHead(200, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(renderRhizohPrometheusMetrics());\n return;\n }\n if (req.method === \"GET\" && pathname.startsWith(\"/infra/traces/\")) {\n const traceId = decodeURIComponent(pathname.slice(\"/infra/traces/\".length));\n const item = await getTraceById(traceId);\n if (!item) return sendJson(res, 404, { ok: false, error: \"trace_not_found\" });\n return sendJson(res, 200, { ok: true, trace: item });\n }\n if (req.method === \"GET\" && pathname === \"/infra/traces\") {\n const u = new URL(String(req.url || \"/infra/traces\"), `http://${req.headers.host || \"localhost\"}`);\n const sessionId = String(u.searchParams.get(\"sessionId\") || \"\").trim();\n const items = sessionId ? await listTracesBySession(sessionId) : [];\n return sendJson(res, 200, { ok: true, sessionId, traces: items });\n }\n\n /** Presence room mesh (C): JOIN / LEAVE / SNAPSHOT / DELTA / REPLAY (HTTP); SUBSCRIBE = SSE stream. */\n if (req.method === \"POST\" && pathname === \"/presence/mesh/join\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid, clientUid: actor.clientUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/leave\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshLeave(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/snapshot\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n sendJson(res, 200, meshSnapshot(roomUid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/delta\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, MAX_MESSAGE_BYTES);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const seq = meshAppendDelta(roomUid, actor.clientUid, {\n node: body?.node,\n projectionPatch: body?.projectionPatch,\n writerSubject: body?.writerSubject\n });\n sendJson(res, 200, { ok: true, roomUid, seq });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/replay\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const fromNodeId = body?.fromNodeId != null ? String(body.fromNodeId) : \"\";\n const fromSeq = body?.fromSeq;\n if (fromNodeId) {\n sendJson(res, 200, meshReplayFromNodeId(roomUid, fromNodeId));\n return;\n }\n sendJson(res, 200, meshReplayFromSeq(roomUid, fromSeq));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/presence/mesh/subscribe\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const roomUid = String(q.searchParams.get(\"roomUid\") || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n const write = (chunk) => {\n try {\n res.write(chunk);\n } catch {\n /* closed */\n }\n };\n const unsub = meshSubscribeSse(roomUid, write);\n const onClose = () => {\n unsub();\n meshLeave(roomUid, actor.clientUid);\n try {\n res.end();\n } catch {\n /* ignore */\n }\n };\n req.on(\"close\", onClose);\n req.on(\"aborted\", onClose);\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, uid: auth.uid, mode: auth.mode, items: await listConnections(auth.uid) });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const id = await createConnection(auth.uid, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"create_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n await updateConnection(auth.uid, id, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"update_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"DELETE\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n await deleteConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"delete_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections/test\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n if (!checkHttpRateLimit(`llm_conn_test:${auth.uid}`, RL_LLM_CONN_TEST_PER_MIN, 60_000)) {\n return sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n }\n const hasBodyKey = !!String(payload?.apiKey || \"\").trim();\n const result = await queryRhizohLlm(\n {\n message: String(payload?.message || \"Ping from Castle Gateway\"),\n context: payload?.context || { source: \"connection_test\" },\n provider: payload?.provider,\n model: payload?.model,\n apiKey: payload?.apiKey || \"\"\n },\n { allowExternalApiKey: true, llmKeySource: hasBodyKey ? \"auto\" : \"env\" }\n );\n logLlmAccess({\n route: \"/llm/connections/test\",\n uid: auth.uid,\n llmKeyBillingOwner: result.llmKeyBillingOwner,\n llmKeyOrigin: result.llmKeyOrigin,\n provider: result.provider,\n model: result.model,\n connectionId: null\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"connection_test_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAgentIdentities(auth.uid);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerAgentIdentity({\n ownerUid: auth.uid,\n agentId: payload?.agentId,\n role: payload?.role,\n personaSeed: payload?.personaSeed || {},\n capabilityLevel: payload?.capabilityLevel\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/agents/identities/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const owns = await getAgentIdentity(id);\n if (!owns || owns.ownerUid !== auth.uid) return sendJson(res, 403, { ok: false, error: \"forbidden\" });\n const payload = await readHttpJson(req);\n const row = await updateAgentIdentity(id, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/academy/events\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/academy/events\"}`);\n const status = String(url.searchParams.get(\"status\") || \"\");\n const items = await listAcademyEvents({ ownerUid: auth.uid, status, limit: 120 });\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/academy/events\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const ev = await queueAcademyEvent({\n type: payload?.type,\n roomId: payload?.roomId,\n ownerUid: auth.uid,\n participants: payload?.participants || [],\n topic: payload?.topic || \"academy\",\n payload: payload?.payload || {}\n });\n sendJson(res, 200, { ok: true, event: ev });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && String(req.url || \"\").startsWith(\"/academy/events/\") && String(req.url || \"\").endsWith(\"/resolve\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const parts = String(req.url || \"\").split(\"/\").filter(Boolean);\n const eventId = parts[2] || \"\";\n try {\n const payload = await readHttpJson(req);\n const out = await resolveAcademyEvent(eventId, payload || {});\n sendJson(res, 200, { ok: true, event: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_resolve_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/memory\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/memory\"}`);\n const scope = url.searchParams.get(\"scope\") === \"agents\" ? \"agents\" : \"users\";\n const id = String(url.searchParams.get(\"id\") || (scope === \"agents\" ? \"\" : auth.uid));\n const kind = String(url.searchParams.get(\"kind\") || \"\");\n const limit = Number(url.searchParams.get(\"limit\") || 40);\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n sendJson(res, 200, { ok: true, scope, id, kind, items: await listMemories({ scope, id, kind, limit }) });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const profile = await getPersonaGoalMemory(auth.uid);\n sendJson(res, 200, { ok: true, profile });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const profile = await setPersonaGoalMemory({\n uid: auth.uid,\n personaPatch: payload?.personaPatch || {},\n goals: Array.isArray(payload?.goals) ? payload.goals : null,\n preferencesPatch: payload?.preferencesPatch || {}\n });\n sendJson(res, 200, { ok: true, profile });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"profile_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/compact\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const out = await autoCompactMemories({ scope, id });\n sendJson(res, 200, { ok: true, scope, id, ...out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_compact_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/context\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const context = await getMemoryContext({\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n query: String(payload?.query || \"\"),\n limit: Number(payload?.limit || 80)\n });\n sendJson(res, 200, { ok: true, context });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_context_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/place-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const placeName = String(payload?.placeName || \"\").trim();\n if (!placeName) return sendJson(res, 400, { ok: false, error: \"placeName_required\" });\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const persona = String(payload?.waypointPersona || payload?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(placeName)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Place: ${placeName}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Give a concise conversational briefing about this place for a walking-route companion.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-place-brief\", placeName, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `PLACE_BRIEF:${placeName} => ${String(summary.reply || \"\").slice(0, 900)}`,\n tags: [\"place\", \"brief\", \"event-layer\"],\n importance: 0.62,\n meta: { placeName }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"place-brief\",\n text: `${placeName} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: {\n placeName,\n directive: summary.directive,\n lat: hit?.lat ? Number(hit.lat) : null,\n lon: hit?.lon ? Number(hit.lon) : null\n }\n });\n sendJson(res, 200, { ok: true, place: hit || null, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"place_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/route-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const waypoints = Array.isArray(payload?.waypoints) ? payload.waypoints.slice(0, 24) : [];\n const out = [];\n for (const wp of waypoints) {\n const name = String(wp?.name || \"\").trim();\n if (!name) continue;\n const persona = String(wp?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(name)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Waypoint: ${name}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Generate a concise spoken briefing for route waypoint.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-route-brief\", waypoint: name, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n out.push({\n name,\n persona,\n place: hit || null,\n reply: summary.reply,\n directive: summary.directive\n });\n }\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `ROUTE_BRIEF: waypoints=${out.length} ${out.map((x) => `${x.name}:${x.persona}`).join(\" | \")}`.slice(0, 1300),\n tags: [\"route\", \"waypoint\", \"brief\", \"event-layer\"],\n importance: 0.74,\n meta: { count: out.length }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"route-brief\",\n text: out.map((x) => `${x.name}:${String(x.reply || \"\").slice(0, 140)}`).join(\" | \").slice(0, 1400),\n roomId: \"studio-main\",\n meta: {\n waypointCount: out.length,\n waypoints: out.map((x) => ({\n name: x.name,\n persona: x.persona,\n lat: x.place?.lat ? Number(x.place.lat) : null,\n lon: x.place?.lon ? Number(x.place.lon) : null\n }))\n }\n });\n sendJson(res, 200, { ok: true, items: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"route_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/pdf-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const title = String(payload?.title || \"document\");\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const providedText = String(payload?.text || \"\");\n const url = String(payload?.url || \"\");\n let content = providedText;\n if (!content && url) content = await fetchTextFromUrl(url);\n if (!content) return sendJson(res, 400, { ok: false, error: \"text_or_supported_url_required\" });\n const summary = await queryRhizohLlm(\n {\n message: `Summarize this document and provide key points + practical actions.\\nTitle:${title}\\nContent:\\n${content.slice(0, 12000)}`,\n context: { source: \"event-layer-pdf-brief\", title },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `DOC_BRIEF:${title} => ${String(summary.reply || \"\").slice(0, 1000)}`,\n tags: [\"document\", \"brief\", \"library\", \"event-layer\"],\n importance: 0.7,\n meta: { title, url: url || null }\n });\n await addTranscript(auth.uid, {\n source: \"library-brief\",\n eventType: \"pdf-brief\",\n text: `${title} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { title }\n });\n sendJson(res, 200, { ok: true, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/pdf/upload\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req, 8 * 1024 * 1024);\n const job = await enqueuePdfJob({\n uid: auth.uid,\n fileName: payload?.fileName || \"document.pdf\",\n bytesBase64: payload?.contentBase64\n });\n sendJson(res, 200, { ok: true, job });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_upload_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/session\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createPublishSession(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session\",\n text: `Publish session armed: ${row.protocol} ${row.target}`.slice(0, 500),\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: row.id, bridge: row.bridge }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/studio/publish/sessions\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listPublishSessions(auth.uid, 80);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/health-check\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const protocol = String(payload?.protocol || \"WHIP\");\n const target = String(payload?.target || \"\");\n const health = await checkPublishTargetHealth(protocol, target);\n if (payload?.sessionId) {\n await updatePublishSession(auth.uid, String(payload.sessionId), {\n status: health.ok ? \"healthy\" : \"degraded\",\n reconnectPolicy: {\n enabled: true,\n backoffSec: [2, 5, 10, 20],\n nextRetryInSec: health.reconnectInSec || 0,\n maxRetries: 8,\n reason: health.reason\n }\n });\n }\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-health\",\n text: `${protocol} ${target} => ${health.ok ? \"healthy\" : \"degraded\"} (${health.reason})`,\n roomId: \"studio-main\",\n meta: { protocol, target, reconnect: !!health.shouldReconnect }\n });\n sendJson(res, 200, { ok: true, health });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_health_check_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/studio/publish/sessions/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updatePublishSession(auth.uid, id, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session-update\",\n text: `Session ${id} -> ${row.status || \"updated\"}`,\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: id, status: row.status, ingestUrl: row.ingestUrl || \"\" }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/transcripts\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/transcripts\"}`);\n const limit = Math.max(1, Math.min(200, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await listTranscripts(auth.uid, limit);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/transcripts\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await addTranscript(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"transcript_write_failed\" });\n }\n return;\n }\n\n /** E1: broadcast intent → GreenRoom capability → transcript + memory artifact */\n if (req.method === \"POST\" && req.url === \"/studio/capabilities/greenroom\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const traceId = String(payload?.traceId || `TRC-${Date.now().toString(36).toUpperCase()}`);\n const title = String(payload?.title || \"Castle Live Broadcast\").slice(0, 200);\n const intentRaw = String(payload?.intentRaw || \"\").slice(0, 800);\n const audienceEstimate = Math.max(0, Math.min(50_000_000, Number(payload?.audienceEstimate) || 1200));\n const roomId = String(payload?.roomId || \"greenroom-main\");\n const transcriptRow = await addTranscript(auth.uid, {\n source: \"rhizoh\",\n eventType: \"BROADCAST_ROUTED\",\n text: `GreenRoom · ${title}${intentRaw ? `\\nIntent: ${intentRaw}` : \"\"}`,\n roomId,\n meta: {\n ack: \"BROADCAST_ROUTED\",\n capability: \"greenroom\",\n intentType: \"broadcast\",\n traceId,\n audienceEstimate,\n artifact: {\n kind: \"broadcast_card\",\n title,\n traceId,\n roomId,\n createdAt: Date.now()\n }\n }\n });\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n text: `GREENROOM_BROADCAST:${title} trace=${traceId}`,\n tags: [\"broadcast\", \"greenroom\", \"artifact\", \"castle-library\"],\n importance: 0.72,\n kind: \"episodic\",\n meta: { traceId, capability: \"greenroom\", transcriptId: transcriptRow.id }\n });\n sendJson(res, 200, {\n ok: true,\n ack: \"BROADCAST_ROUTED\",\n traceId,\n transcript: transcriptRow,\n replayPath: `/replay/${encodeURIComponent(traceId)}`,\n sharePath: `/replay/${encodeURIComponent(traceId)}`\n });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"greenroom_capability_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/metadata/search\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/metadata/search\"}`);\n const q = String(url.searchParams.get(\"q\") || \"\");\n const limit = Math.max(1, Math.min(300, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await searchMetadata(auth.uid, q, limit);\n sendJson(res, 200, { ok: true, q, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerDevice(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, device: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"device_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const lat = Number(payload?.lat);\n const lon = Number(payload?.lon);\n const speed = Math.max(0, Number(payload?.speed || 0));\n const cfgCenterLat = Number(process.env.CASTLE_GEOFENCE_CENTER_LAT || 41.0082);\n const cfgCenterLon = Number(process.env.CASTLE_GEOFENCE_CENTER_LON || 28.9784);\n const cfgRadiusM = Math.max(100, Number(process.env.CASTLE_GEOFENCE_RADIUS_M || 55000));\n const cfgSpeedCap = Math.max(1, Number(process.env.CASTLE_SPEED_CAP_MPS || 35));\n const dist = Number.isFinite(lat) && Number.isFinite(lon) ? haversineMeters(cfgCenterLat, cfgCenterLon, lat, lon) : 0;\n const geofenceOk = !Number.isFinite(dist) ? true : dist <= cfgRadiusM;\n const speedOk = speed <= cfgSpeedCap;\n const failsafe = !(geofenceOk && speedOk);\n const safety = {\n geofenceOk,\n speedOk,\n failsafe,\n distanceFromCenterM: Number.isFinite(dist) ? dist : null,\n speedCapMps: cfgSpeedCap\n };\n const row = await appendTelemetry(auth.uid, deviceId, {\n lat,\n lon,\n speed,\n battery: payload?.battery,\n mode: payload?.mode || (failsafe ? \"FAILSAFE_HOLD\" : \"ACTIVE\"),\n raw: payload?.raw || {}\n });\n await updateDevice(auth.uid, deviceId, {\n status: failsafe ? \"failsafe\" : \"active\",\n lastTelemetryAt: row.ts,\n safetyState: safety\n });\n if (failsafe) {\n await addTranscript(auth.uid, {\n source: \"robotics-guardrails\",\n eventType: \"failsafe\",\n text: `Device ${deviceId} failsafe triggered geofenceOk=${geofenceOk} speedOk=${speedOk}`,\n roomId: \"studio-main\",\n meta: { deviceId, safety }\n });\n }\n sendJson(res, 200, { ok: true, telemetry: row, safety, command: failsafe ? \"FAILSAFE_HOLD\" : \"CONTINUE\" });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"telemetry_ingest_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listTelemetry(auth.uid, 150);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createAutomation(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"automation-scheduled\",\n text: `Automation scheduled: ${row.title} at ${new Date(row.scheduleAt).toLocaleString()}`,\n roomId: \"studio-main\",\n meta: { automationId: row.id, action: row.action, deviceId: row.deviceId }\n });\n sendJson(res, 200, { ok: true, automation: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"automation_create_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAutomations(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listDevices(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const devs = await listDevices(auth.uid, 500);\n const d = devs.find((x) => x.id === deviceId);\n if (!d) return sendJson(res, 404, { ok: false, error: \"device_not_found\" });\n const action = String(payload?.action || \"\");\n const caps = Array.isArray(d.capabilityProfile) ? d.capabilityProfile : [];\n if (action && caps.length && !caps.includes(action) && !caps.includes(\"*\")) {\n return sendJson(res, 400, { ok: false, error: \"unsupported_action_for_device\", capabilityProfile: caps });\n }\n const cmd = await enqueueCommand(auth.uid, {\n deviceId,\n action: action || \"NOOP\",\n params: payload?.params || {},\n adapter: payload?.adapter || d.adapter || \"websocket\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-command-queue\",\n eventType: \"command-queued\",\n text: `${cmd.deviceId} -> ${cmd.action}`,\n roomId: \"studio-main\",\n meta: { commandId: cmd.id, adapter: cmd.adapter }\n });\n sendJson(res, 200, { ok: true, command: cmd });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listCommands(auth.uid, 200);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/robotics/commands/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updateCommandStatus(auth.uid, id, payload?.status || \"sent\");\n sendJson(res, 200, { ok: true, command: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/rhizoh/bridge\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const result = await runRhizohBrainV2({\n message: String(payload?.message || \"robotics bridge command\"),\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n worldState: payload?.worldState || {},\n deviceState: payload?.deviceState || {},\n intent: \"robotics-mechanics\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-bridge\",\n eventType: \"robotics-command\",\n text: `Bridge -> ${String(result.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { directive: result.directive, agentId: payload?.agentId || \"\" }\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"robotics_bridge_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/planner/graph\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const graph = buildTaskActionGraph(String(payload?.task || \"\"), payload?.context || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"planner-graph\",\n text: `Planner graph built: ${graph.taskTitle} nodes=${graph.nodes.length}`,\n roomId: \"studio-main\",\n meta: { taskTitle: graph.taskTitle, nodeCount: graph.nodes.length }\n });\n sendJson(res, 200, { ok: true, graph });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"planner_graph_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerSocialChannel(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, channel: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_channel_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listSocialChannels(auth.uid, 100);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/broadcast\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const text = String(payload?.text || \"\").slice(0, 1200);\n const channels = await listSocialChannels(auth.uid, 100);\n const enabled = channels.filter((c) => c.status === \"enabled\");\n const deliveries = [];\n for (const c of enabled) {\n const out = await deliverSocialWithRetry(c, text);\n deliveries.push({ id: c.id, platform: c.platform, ...out });\n }\n const okCount = deliveries.filter((d) => d.ok).length;\n await addTranscript(auth.uid, {\n source: \"social-layer\",\n eventType: \"social-broadcast\",\n text: `Broadcast delivered ${okCount}/${enabled.length} channel(s): ${text.slice(0, 240)}`,\n roomId: \"studio-main\",\n meta: { deliveries }\n });\n sendJson(res, 200, { ok: true, sentChannels: okCount, totalChannels: enabled.length, deliveries });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_broadcast_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/ethics/programs\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, ...buildEthicsPrograms() });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands/dispatch\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const all = await listCommands(auth.uid, 200);\n const queued = all.filter((c) => String(c.status || \"queued\") === \"queued\").slice(0, 50);\n const devices = await listDevices(auth.uid, 500);\n const byId = new Map(devices.map((d) => [d.id, d]));\n const results = [];\n for (const cmd of queued) {\n const dev = byId.get(cmd.deviceId);\n if (!dev) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: \"device_not_found\" });\n continue;\n }\n try {\n const dispatched = await dispatchCommandToAdapter(dev, cmd);\n await updateCommandStatus(auth.uid, cmd.id, \"sent\");\n results.push({ id: cmd.id, ok: true, adapter: dispatched.adapter, via: dispatched.via });\n } catch (error) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: String(error?.message || \"dispatch_failed\") });\n }\n }\n await addTranscript(auth.uid, {\n source: \"robotics-command-dispatch\",\n eventType: \"command-dispatch-batch\",\n text: `Dispatch batch size=${results.length} ok=${results.filter((x) => x.ok).length}`,\n roomId: \"studio-main\",\n meta: { results: results.slice(0, 30) }\n });\n sendJson(res, 200, { ok: true, processed: results.length, results });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"dispatch_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/pdf/jobs/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n const job = await getPdfJob(auth.uid, id);\n if (!job) return sendJson(res, 404, { ok: false, error: \"pdf_job_not_found\" });\n sendJson(res, 200, { ok: true, job });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const row = await appendMemory({\n scope,\n id,\n text: payload?.text,\n tags: payload?.tags || [],\n importance: payload?.importance,\n kind: payload?.kind || \"episodic\",\n meta: { uid: auth.uid, ...(payload?.meta || {}) }\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.externalTruth) {\n try {\n if (process.env.RHIZOH_EXTERNAL_TRUTH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_truth:${ip}`, RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n sendJson(res, 200, buildRhizohExternalGroundTruthPayload(req));\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.productOutcome) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const rawBody = await readHttpBodyText(req, 64 * 1024);\n const body = rawBody.trim() ? safeJsonParse(rawBody) : null;\n if (!body || typeof body !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"invalid_json_payload\" });\n return;\n }\n const sigHdr = String(req.headers?.[\"x-rhizoh-outcome-signature\"] || \"\").trim();\n const hmacV = verifyRhizohOutcomeHmac(rawBody, sigHdr);\n if (!hmacV.ok) {\n sendJson(res, 401, { ok: false, error: hmacV.error || \"outcome_hmac_invalid\" });\n return;\n }\n const srcTokHdr = String(req.headers?.[\"x-rhizoh-outcome-source-token\"] || \"\").trim();\n const srcV = verifyRhizohOutcomeSourceToken(body.source, srcTokHdr);\n if (!srcV.ok) {\n sendJson(res, 401, { ok: false, error: srcV.error || \"outcome_source_token_invalid\" });\n return;\n }\n const subjectKey = String(body.subjectKey || \"\").trim().slice(0, 128);\n if (\n subjectKey &&\n !checkHttpRateLimit(\n `rhizoh_product_outcome_subj:${subjectKey}`,\n RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN,\n 60_000\n )\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_subject\" });\n return;\n }\n const result = ingestRhizohProductOutcomeHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n schemaVersion: \"1.1.0\",\n aggregate: result.aggregate,\n integrity: result.integrity\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"outcome_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.productOutcomeAggregate) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome_agg:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const u = new URL(String(req.url || \"/\"), \"http://localhost\");\n const cohortId = u.searchParams.get(\"cohortId\") || \"\";\n const decisionFingerprint = u.searchParams.get(\"decisionFingerprint\") || \"\";\n const limit = u.searchParams.get(\"limit\") || \"50\";\n const rows = listRhizohProductOutcomeAggregates({\n cohortId: cohortId || undefined,\n decisionFingerprint: decisionFingerprint || undefined,\n limit: Number(limit) || 50\n });\n sendJson(res, 200, { ok: true, schemaVersion: \"1.1.0\", rows });\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.externalLossBatch) {\n try {\n if (process.env.RHIZOH_EXTERNAL_LOSS_BATCH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_loss_batch:${ip}`, RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const result = ingestRhizohExternalLossBatchHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, { ok: true, accepted: result.accepted ?? 0 });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"batch_failed\") });\n }\n return;\n }\n\n function moderationAdminOk(req) {\n const expected = String(process.env.CASTLE_MODERATION_ADMIN_KEY || \"\").trim();\n if (!expected) return false;\n const got = String(req.headers[\"x-castle-moderation-key\"] || \"\").trim();\n return got.length > 0 && got === expected;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.opsAbuseReport) {\n try {\n const body = await readHttpJson(req, 32 * 1024);\n const auth = await resolveHttpUser(req);\n const entry = submitAbuseReportV0({\n reporterUid: auth.ok ? auth.uid : null,\n targetUid: body?.targetUid ?? null,\n category: body?.category,\n detail: body?.detail,\n traceId: body?.traceId ?? null\n });\n sendJson(res, 202, { ok: true, reportId: entry.id, status: entry.status });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsModerationQueue) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n queue: listModerationQueueV0(100),\n softBlocked: listSoftBlockedUsersV0()\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-block\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const out = softBlockUserV0(body?.uid, body?.reason);\n sendJson(res, out.ok ? 200 : 400, out);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-unblock\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n sendJson(res, 200, removeSoftBlockV0(body?.uid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsAgentSnapshots) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n const limit = Number(new URL(req.url || \"\", \"http://local\").searchParams.get(\"limit\") || 50);\n sendJson(res, 200, { ok: true, snapshots: listRecentAgentSnapshotsV0(limit) });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsHardeningStatus) {\n const auth = await resolveHttpUser(req);\n const principal = auth.ok ? `uid:${auth.uid}` : null;\n const gclSnap = await getGlobalCostLedgerSnapshotV0(principal);\n const economicStrategy = evaluateEconomicStrategyL3V0(gclSnap);\n const hardeningTenantId = resolveHardeningTenantIdV0(auth, req);\n const unifiedState = await buildUnifiedStateNarrativeV0({\n tenantId: hardeningTenantId ?? undefined,\n platformScope: !hardeningTenantId,\n principal\n });\n sendJson(res, 200, {\n ok: true,\n schema: \"rhizoh.ops.hardening_status.v0\",\n containment: readAgentContainmentConfigV0(),\n cost: readCostContainmentConfigV0(),\n gcl: {\n backend: await getGclBackendStatusV0(),\n snapshot: gclSnap\n },\n economicStrategyL3: economicStrategy,\n phasedRollout: await getPhasedRolloutStatsV0(),\n tier: readPhasedRolloutTierV0(),\n principalCost: auth.ok ? await getCostContainmentStatsV0(principal) : null,\n stressTaxonomy: verifyStressTaxonomyCoverageV0(),\n stressConflictResolution: verifyStressConflictResolutionV0(),\n resolutionStabilityEnvelope: verifyResolutionStabilityEnvelopeV0(),\n unifiedState: {\n tenantScope: {\n tenantId: unifiedState.tenantScope?.tenantId,\n scope: unifiedState.tenantScope?.scope,\n noGlobalDerivedState: unifiedState.tenantScope?.noGlobalDerivedState\n },\n narrativeFingerprint: unifiedState.narrativeFingerprint,\n screenshotScopeWatermark: unifiedState.screenshotScopeWatermark,\n interpretationSafetyContract: unifiedState.interpretationSafetyContract,\n interpretationUxContract: {\n schema: unifiedState.interpretationUxContract?.schema,\n layerOrder: unifiedState.interpretationUxContract?.layerOrder,\n narrativeNeverVisuallyDominant: unifiedState.interpretationUxContract?.narrativeNeverVisuallyDominant\n },\n governance: {\n narrativeIsNotDecisionLayer: unifiedState.governance.narrativeIsNotDecisionLayer,\n displayOrder: unifiedState.governance.displayOrder,\n trustPosture: unifiedState.governance.trustPosture,\n interpretationUxContract: unifiedState.governance.interpretationUxContract?.schema\n },\n humanOps: {\n decisionTier: unifiedState.humanOps?.humanDecisionScaling?.tier,\n decisionOwner: unifiedState.humanOps?.humanDecisionScaling?.decisionOwner,\n opsRouteId: unifiedState.humanOps?.humanDecisionOpsRunbook?.routing?.routeId,\n slaAckMinutes: unifiedState.humanOps?.humanDecisionOpsRunbook?.sla?.ackMinutes,\n uiDisclaimerRequired: unifiedState.humanOps?.narrativeUiSafety?.displayRules?.requireDisclaimerBanner,\n misreadHighResidual: unifiedState.humanOps?.misreadSimulation?.highResidualCount,\n propagationHighResidual: unifiedState.humanOps?.socialPropagationSimulation?.highResidualCount,\n worstWatermarkSurvivability:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.worstWatermarkSurvivability,\n dominantDistortionSource:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.dominantDistortionSource\n },\n culturalRisk: {\n perceptualRiskClass: unifiedState.culturalRisk?.perceptualRiskClass,\n trustFork: unifiedState.culturalRisk?.trustDynamics?.fork,\n trustDecayScore: unifiedState.culturalRisk?.trustDynamics?.scores?.trustDecay,\n mythologyScore: unifiedState.culturalRisk?.trustDynamics?.scores?.mythology,\n confidenceDetachmentExposure:\n unifiedState.culturalRisk?.trustDynamics?.axes?.confidenceDetachmentExposure\n },\n appliedSystemsLayer: {\n schema: unifiedState.appliedSystemsLayer?.schema,\n researchOutputType: unifiedState.appliedSystemsLayer?.researchGovernance?.outputType,\n roboticsBlockActuation: unifiedState.appliedSystemsLayer?.roboticsGrounding?.blockActuation,\n spiralRumorCount:\n unifiedState.appliedSystemsLayer?.spiralMMOGameKernel?.gameLayer?.rumors?.length\n },\n actionSemanticGovernance: {\n schema: unifiedState.actionSemanticGovernance?.schema,\n highRiskActionCount: unifiedState.actionSemanticGovernance?.highRiskActionCount,\n loopClosed: unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.loopClosed,\n authorityCollapseMitigated:\n unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.risks?.authorityCollapse\n ?.mitigated\n },\n actionContextResolution: {\n schema: unifiedState.actionContextResolution?.schema,\n contextFingerprint: unifiedState.actionContextResolution?.contextFingerprint?.fingerprintHash,\n domainCollision: unifiedState.actionContextResolution?.domainCollision?.detected,\n deployAgentSpiralBlocked:\n unifiedState.actionContextResolution?.actionOverloadingRisk?.deployAgentExample\n ?.spiral_mmo === \"blocked\",\n roboticsActuationEligible:\n unifiedState.actionContextResolution?.roboticsActuationCorrectness?.actuationEligible\n },\n epistemicCoherence: {\n schema: unifiedState.epistemicCoherence?.schema,\n verdict: unifiedState.epistemicCoherence?.systemCoherence?.verdict,\n coherenceScore: unifiedState.epistemicCoherence?.systemCoherence?.score,\n contradictionCount: unifiedState.epistemicCoherence?.crossLayerContradictions?.count,\n operatorDecision: unifiedState.epistemicCoherence?.uxCompression?.operatorDecision,\n headlineTr: unifiedState.epistemicCoherence?.uxCompression?.headline?.tr,\n falseSafetyWarning: Boolean(\n unifiedState.epistemicCoherence?.uxCompression?.falseSafetyWarning\n )\n },\n coherenceAuthorityBoundary: {\n schema: unifiedState.coherenceAuthorityBoundary?.schema,\n bindingValid: unifiedState.coherenceAuthorityBoundary?.eclAuthorityBinding?.valid,\n canExecute: unifiedState.coherenceAuthorityBoundary?.contract?.can_execute,\n mandatoryBannerTr:\n unifiedState.coherenceAuthorityBoundary?.disclaimers?.mandatoryBanner?.tr\n },\n decisionLatencyGovernance: {\n schema: unifiedState.decisionLatencyGovernance?.schema,\n latencyTier: unifiedState.decisionLatencyGovernance?.latencyTier,\n inflationScore: unifiedState.decisionLatencyGovernance?.latencyInflation?.score,\n fastPathEligible: unifiedState.decisionLatencyGovernance?.fastPath?.eligible,\n primaryActionId: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.primaryActionId,\n ackSlaMinutes: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.ackSlaMinutes,\n certaintyCap: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.certaintyCap,\n uncertaintyItems:\n unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.uncertaintyEnvelope?.itemCount\n },\n decisionPacketUncertaintyBoundary: {\n schema: unifiedState.decisionPacketUncertaintyBoundary?.schema,\n invariantValid: unifiedState.decisionPacketUncertaintyBoundary?.invariantCheck?.valid,\n centralInvariant: unifiedState.decisionPacketUncertaintyBoundary?.centralInvariant\n },\n epistemicDecisionPacing: {\n schema: unifiedState.epistemicDecisionPacing?.schema,\n queueSaturationTier:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation?.tier,\n operatorProcessingLatency:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation\n ?.operatorProcessingLatency,\n postureWindowCount:\n unifiedState.epistemicDecisionPacing?.temporaryStaticPostureWindows\n ?.temporaryStaticPostureWindows?.windowCount,\n executionBoundariesValid:\n unifiedState.epistemicDecisionPacing?.executionBoundaries?.valid,\n grantsExecutionApproval: unifiedState.epistemicDecisionPacing?.grantsExecutionApproval\n },\n epistemicTemporalCoherence: {\n schema: unifiedState.epistemicTemporalCoherence?.schema,\n allInvariantsValid: unifiedState.epistemicTemporalCoherence?.allInvariantsValid,\n temporalAlignmentValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.temporalAlignment?.valid,\n crossWindowGuardValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard?.valid,\n delayedTruthValid: unifiedState.epistemicTemporalCoherence?.invariants?.delayedTruthRule?.valid,\n reconciliationRequired:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard\n ?.reconciliation?.required\n },\n realityDriftObserver: {\n schema: unifiedState.realityDriftObserver?.schema,\n driftActive: unifiedState.realityDriftObserver?.driftObserverSummary?.active,\n misapprehensionShapeCount:\n unifiedState.realityDriftObserver?.misapprehensionShapeCatalog?.shapeCount,\n criticalShapeCount:\n unifiedState.realityDriftObserver?.driftObserverSummary?.criticalShapeCount,\n roboticsMismatchActive:\n unifiedState.realityDriftObserver?.roboticsFeedbackMismatch?.active\n },\n driftCausality: {\n schema: unifiedState.driftCausality?.schema,\n shapesWithCausalExplanation: unifiedState.driftCausality?.rdolLinkage?.shapesWithCausalExplanation,\n activeDomainCount: unifiedState.driftCausality?.causalitySummary?.activeDomainCount,\n propagationCausalityActive: unifiedState.driftCausality?.causalDomains?.propagation?.active\n },\n productReadiness: unifiedState.productReadiness,\n stateLayers: {\n raw: unifiedState.stateLayers.raw,\n derived: {\n systemState: unifiedState.stateLayers.derived.systemState,\n narrative: unifiedState.stateLayers.derived.narrative,\n validation: unifiedState.stateLayers.derived.validation\n },\n policy: {\n binding: unifiedState.stateLayers.policy.binding,\n decisionOwner: unifiedState.stateLayers.policy.decisionOwner,\n prohibitedActions: unifiedState.stateLayers.policy.prohibitedActions\n }\n },\n systemState: unifiedState.systemState,\n interpretation: unifiedState.interpretation,\n validation: {\n trustPosture: unifiedState.validation.trustPosture,\n adjustedConfidence: unifiedState.validation.adjustedConfidence,\n agreement: unifiedState.validation.confidenceDecomposition.agreement,\n divergenceScore: unifiedState.validation.divergence.divergenceScore,\n validated: unifiedState.validation.divergence.validated,\n uncertaintyTags: unifiedState.validation.uncertainty.tags.map((t) => t.id),\n flags: unifiedState.validation.divergence.flags.map((f) => f.id)\n },\n compression: unifiedState.compression\n }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === RHIZOH_VOICE_TRANSCRIBE_ROUTE_V3) {\n try {\n const payload = await readHttpJson(req);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (\n !checkHttpRateLimit(`rhizoh_voice_transcribe:${rlKey}`, RL_RHIZOH_VOICE_TRANSCRIBE_PER_MIN, 60_000)\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const voiceEnv = rhizohVoiceTranscribeEnvV3();\n if (!voiceEnv.any) {\n sendJson(res, 503, {\n ok: false,\n error: \"voice_asr_not_configured\",\n env: voiceEnv\n });\n return;\n }\n const result = await runRhizohVoiceTranscribeV3(payload);\n sendJson(res, result.ok ? 200 : 422, {\n ...result,\n traceId: String(payload?.traceId || \"\"),\n atMs: Date.now()\n });\n } catch (error) {\n sendJson(res, 500, {\n ok: false,\n error: \"voice_transcribe_failed\",\n detail: String(error?.message || error)\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === rhizohRuntime.routes.rhizohLlm) {\n try {\n const payload = await readHttpJson(req);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n logRhizohHealth(\"gateway_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: String(payload?.traceId || \"\"),\n auth: auth.ok ? \"ok\" : \"anon\",\n ip\n });\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (!checkHttpRateLimit(`rhizoh_llm:${rlKey}`, RL_RHIZOH_LLM_PER_MIN, 60_000)) {\n const rlTaxonomy = classifyStressResponseV0({ code: \"rate_limit_exceeded\" });\n return sendJson(res, 429, {\n ok: false,\n error: \"rate_limit_exceeded\",\n reply: rlTaxonomy.userMessageTr || \"İstek sınırı aşıldı. Kısa süre sonra tekrar deneyin.\",\n stressClass: rlTaxonomy.stressClass,\n responseAction: rlTaxonomy.responseAction,\n stressMatrix: rlTaxonomy.matrix,\n stressInterpretable: rlTaxonomy.interpretable,\n stressConfidence: rlTaxonomy.stressConfidence,\n actionConfidence: rlTaxonomy.actionConfidence,\n actionSoftened: rlTaxonomy.actionSoftened,\n responseActionStrict: rlTaxonomy.responseActionStrict,\n actionInterpretable: rlTaxonomy.actionInterpretable,\n conflictResolution: rlTaxonomy.conflictResolution,\n directive: \"NONE\"\n });\n }\n\n const requireExplicit = process.env.CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE === \"1\";\n const keyMode = normalizeClientLlmKeySource(payload?.llmKeySource ?? payload?.keySource);\n if (keyMode === null) {\n return sendJson(res, 400, {\n ok: false,\n error: \"invalid_llm_key_source\",\n reply: \"llmKeySource geçersiz. Kullanın: env | user_connection | auto\",\n directive: \"NONE\"\n });\n }\n if (requireExplicit && keyMode === \"auto\") {\n return sendJson(res, 400, {\n ok: false,\n error: \"llm_key_source_required\",\n reply: \"Sunucu CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE=1: istekte llmKeySource (env veya user_connection) zorunlu.\",\n directive: \"NONE\"\n });\n }\n\n const conn = auth.ok ? await resolveConnection(auth.uid, String(payload?.connectionId || \"\")) : null;\n\n if (keyMode === \"user_connection\") {\n if (!auth.ok) {\n return sendJson(res, 401, {\n ok: false,\n error: \"llm_auth_required_for_user_connection\",\n reply: \"Kayıtlı LLM bağlantısı için giriş gerekli.\",\n directive: \"NONE\"\n });\n }\n if (!conn?.apiKey) {\n return sendJson(res, 400, {\n ok: false,\n error: \"user_llm_connection_required\",\n reply: \"Geçerli bir LLM bağlantısı yok; Studio’da bağlantı ekleyin veya llmKeySource=env kullanın.\",\n directive: \"NONE\"\n });\n }\n }\n\n const safePayload = { ...(payload || {}) };\n delete safePayload.apiKey;\n delete safePayload.llmKeySource;\n delete safePayload.keySource;\n\n let resolvedProvider = payload?.provider;\n let resolvedModel = payload?.model;\n let connApiKey = \"\";\n\n if (keyMode === \"user_connection\") {\n resolvedProvider = conn.provider;\n resolvedModel = conn.model;\n connApiKey = conn.apiKey || \"\";\n } else if (keyMode === \"env\") {\n connApiKey = \"\";\n } else {\n resolvedProvider = conn?.provider || payload?.provider;\n resolvedModel = conn?.model || payload?.model;\n connApiKey = conn?.apiKey || \"\";\n }\n\n const { result, traceId, turnLatencyMs, spinePhases, sampledTrace } = await rhizohGatewayTurn({\n safePayload,\n auth,\n keyMode,\n conn,\n resolvedProvider,\n resolvedModel,\n connApiKey\n });\n logRhizohHealth(\"llm_response\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId,\n turnLatencyMs: Number(turnLatencyMs || 0),\n keyMode\n });\n sendJson(res, 200, {\n ...result,\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n traceId,\n turnLatencyMs,\n ...(spinePhases ? { spinePhases } : {}),\n ...(sampledTrace !== undefined ? { sampledTrace } : {})\n });\n } catch (error) {\n const msg = String(error?.message || \"\");\n const code = error?.code || \"\";\n let status = 500;\n if (msg === \"rate_limit_exceeded\") status = 429;\n else if (\n code === \"cost_hard_limit\" ||\n code === \"phased_rollout_capacity\" ||\n error?.containment === true ||\n String(code).startsWith(\"agent_\")\n ) {\n status = 429;\n } else if (code === \"user_soft_blocked\" || code === \"prompt_abuse_detected\") status = 403;\n else if ([\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(code) || msg.includes(\"missing_api_key_for_\"))\n status = 400;\n let rhizohFailureKind = \"provider_error\";\n if (msg === \"rate_limit_exceeded\" || status === 429) rhizohFailureKind = \"rate_limit\";\n else if (status === 403) rhizohFailureKind = \"policy_block\";\n else if (status === 400) rhizohFailureKind = \"client_config\";\n else if (msg.startsWith(\"provider_http_\")) {\n const st = Number(msg.replace(\"provider_http_\", \"\"));\n if (st === 408 || st === 504) rhizohFailureKind = \"timeout\";\n else rhizohFailureKind = \"provider_error\";\n } else if (/timeout|timed out|ETIMEDOUT/i.test(msg)) rhizohFailureKind = \"timeout\";\n\n const providerHttpMatch = msg.match(/^provider_http_(\\d{3})$/);\n const stressTaxonomy =\n error?.stressClass != null\n ? {\n stressClass: error.stressClass,\n responseAction: error.responseAction,\n stressMatrix: error.stressMatrix,\n stressInterpretable: error.stressInterpretable !== false,\n stressConfidence: error.stressConfidence,\n actionConfidence: error.actionConfidence,\n actionSoftened: error.actionSoftened,\n responseActionStrict: error.responseActionStrict,\n actionInterpretable: error.actionInterpretable,\n conflictResolution: error.conflictResolution,\n stressSecondary: error.stressSecondary,\n responseActions: error.responseActions\n }\n : (() => {\n const t = classifyStressResponseV0({\n code: code || msg,\n rhizohFailureKind,\n providerHttpStatus: providerHttpMatch\n ? Number(providerHttpMatch[1])\n : null\n });\n return {\n stressClass: t.stressClass,\n responseAction: t.responseAction,\n stressMatrix: t.matrix,\n stressInterpretable: t.interpretable,\n stressConfidence: t.stressConfidence,\n actionConfidence: t.actionConfidence,\n actionSoftened: t.actionSoftened,\n responseActionStrict: t.responseActionStrict,\n actionInterpretable: t.actionInterpretable,\n conflictResolution: t.conflictResolution,\n stressSecondary: t.stressSecondary,\n responseActions: t.responseActions\n };\n })();\n sendJson(res, status, {\n ok: false,\n rhizohFailureKind,\n ...(providerHttpMatch ? { providerHttpStatus: Number(providerHttpMatch[1]) } : {}),\n error: code || msg || \"rhizoh_llm_failed\",\n reply:\n error?.reply ||\n (status === 500 ? \"Rhizoh bağlantısı geçici olarak kesildi.\" : msg),\n directive: error?.directive || \"NONE\",\n ...stressTaxonomy\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicSeal) {\n try {\n if (!EPISTEMIC_SEAL_SECRET) {\n sendJson(res, 503, { ok: false, error: \"epistemic_seal_disabled\" });\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`epistemic_seal:${ip}`, RL_EPISTEMIC_SEAL_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const truth = body.truth_contract;\n if (!truth || typeof truth !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"truth_contract_required\" });\n return;\n }\n const sealBody = {\n truth_contract: truth,\n runtime_hash: String(body.runtime_hash || \"\"),\n model_route: {\n provider: body.model_route?.provider ?? null,\n model: body.model_route?.model ?? null\n },\n memory_digest: String(body.memory_digest || \"\"),\n world_snapshot_hash: String(body.world_snapshot_hash || \"\"),\n timestamp: Number(body.timestamp) || Date.now()\n };\n const canonical = canonicalEpistemicSealString(sealBody);\n const { hash, signature } = hashAndSignEpistemicSeal(canonical, EPISTEMIC_SEAL_SECRET);\n recordGenesisEpistemicSealIssued(hash);\n const attestation = {\n schema: EPISTEMIC_SEAL_SCHEMA,\n algorithm: \"SHA-256+HMAC-SHA256\",\n keyHint: process.env.CASTLE_EPISTEMIC_SEAL_SECRET ? \"CASTLE_EPISTEMIC_SEAL_SECRET\" : \"CASTLE_GATEWAY_TOKEN\",\n issuedAt: Date.now(),\n gateway: { service: \"castle-gateway\", listenPort: PORT }\n };\n sendJson(res, 200, {\n ok: true,\n hash,\n signature,\n attestation,\n canonicalBytes: Buffer.byteLength(canonical, \"utf8\")\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"seal_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicLogsBatch) {\n try {\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const auth = await resolveHttpUser(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`epistemic_logs:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 120) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const persisted = await persistEpistemicLedgerBatch(auth.uid, entries);\n recordGenesisEpistemicLedgerPersisted(persisted.persisted);\n // Internal cognition layer: forecast stays gateway/ledger-only (no client exposure).\n try {\n await persistEpistemicForecastBatch(auth.uid, persisted.normalized || []);\n } catch {\n /* forecast failures must not break primary ledger ingest */\n }\n sendJson(res, 200, {\n ok: true,\n persisted: persisted.persisted,\n mode: persisted.mode,\n latest: persisted.normalized?.[persisted.normalized.length - 1] || null\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"epistemic_log_ingest_failed\") });\n }\n return;\n }\n\n sendJson(res, 404, { ok: false, error: \"not_found\" });\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\nspiralWssForGenesis = wss;\nconst orchestrator = createOrchestrator();\nconst clientStats = new Map();\nlet broadcasterClientId = null;\nconst spiralState = {\n rooms: [{ id: \"room-default\", name: \"Spiral Default Arena\" }],\n activeByClient: new Map(),\n characters: new Map() // id -> {id,name,role,roomId,source}\n};\n\n/** Castle social rooms — keyed by `castleRoomKey` from {@link WS_MESSAGE.CASTLE_SOCIAL_PULSE}. */\nconst castleSocialRoomState = new Map();\nconst CASTLE_SOCIAL_MEMBER_TTL_MS = 90_000;\nconst CASTLE_SOCIAL_MAX_PULSE_BYTES = 8192;\n\nfunction removeCastleSocialClientFromAllRooms(clientId) {\n const cid = String(clientId || \"\");\n if (!cid) return;\n for (const room of castleSocialRoomState.values()) {\n room.members.delete(cid);\n }\n}\n\nfunction buildCastleSocialRoomPayload(roomKey) {\n const room = castleSocialRoomState.get(roomKey);\n if (!room) return null;\n const now = Date.now();\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const roster = [...room.members.values()].map((m) => ({\n userId: m.userId,\n lastMs: m.lastMs,\n ...m.sp\n }));\n const peerFeeds =\n room.walPeers && room.walPeers.size\n ? [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }))\n : [];\n return { castleRoomKey: roomKey, seq: room.seq, roster, peerFeeds, ts: now };\n}\n\nconst CASTLE_WAL_PEER_MAX_FEED_BYTES = 48 * 1024;\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {{ ok: boolean, code?: string, reason?: string }} gate\n */\nfunction rejectWalPeerIngressV0(socket, gate) {\n const code = gate.code || \"wal_peer_rejected\";\n recordWalPeerRejectV0(code);\n recordSubstrateOtelEventV0(\"wal_peer_reject\", { code, reason: String(gate.reason || \"\").slice(0, 120) });\n socket.send(\n JSON.stringify(\n createEnvelope(WS_MESSAGE.ERROR, {\n error: gate.reason || \"WAL peer ingress rejected.\",\n code\n })\n )\n );\n}\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {Record<string, unknown> | null | undefined} walPeerFeed\n * @returns {boolean}\n */\nfunction gateWalPeerIngressV0(socket, walPeerFeed) {\n const authGate = assertWalPeerSocketAuthorityV0(socket);\n if (!authGate.ok) {\n rejectWalPeerIngressV0(socket, authGate);\n return false;\n }\n const sigGate = validateWalPeerFeedSignatureV0(\n walPeerFeed && typeof walPeerFeed === \"object\" ? walPeerFeed : {}\n );\n if (!sigGate.ok) {\n rejectWalPeerIngressV0(socket, sigGate);\n return false;\n }\n return true;\n}\n\nfunction handleCastleWalPeerFeed(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_WAL_PEER_MAX_FEED_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_WAL_PEER_FEED too large.\" })));\n return;\n }\n const walPeerFeed = p.walPeerFeed && typeof p.walPeerFeed === \"object\" ? p.walPeerFeed : {};\n if (!gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const castleRoomKey = String(p.castleRoomKey || \"wal:convergence\").slice(0, 64);\n const castleId = String(p.castleId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId,\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: room.members.get(socket.clientId)?.sp ?? {}\n });\n const peerFeeds = [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }));\n const body = {\n castleRoomKey,\n seq: room.seq,\n peerFeeds,\n ts: now\n };\n const env = createEnvelope(WS_MESSAGE.CASTLE_WAL_PEER_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys(), ...room.walPeers.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction handleCastleSocialPulse(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_SOCIAL_MAX_PULSE_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_SOCIAL_PULSE too large.\" })));\n return;\n }\n const castleRoomKey = String(p.castleRoomKey || \"default\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n const sp = p.socialPulse && typeof p.socialPulse === \"object\" ? p.socialPulse : {};\n const walPeerFeed =\n p.walPeerFeed && typeof p.walPeerFeed === \"object\"\n ? p.walPeerFeed\n : sp.walPeerFeed && typeof sp.walPeerFeed === \"object\"\n ? sp.walPeerFeed\n : null;\n if (walPeerFeed && !gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const spNext = walPeerFeed ? { ...sp, walPeerFeed } : { ...sp };\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: spNext\n });\n if (walPeerFeed) {\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId: String(p.castleId || userId).slice(0, 64),\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n }\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const body = buildCastleSocialRoomPayload(castleRoomKey);\n if (!body) return;\n const env = createEnvelope(WS_MESSAGE.CASTLE_SOCIAL_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcast(msgObj) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcastState() {\n broadcast(\n createEnvelope(WS_MESSAGE.BROADCAST_STATE, {\n broadcasterClientId,\n viewers: [...wss.clients].filter((c) => c.readyState === 1).length\n })\n );\n}\n\nfunction currentSpiralStatePayload() {\n return {\n rooms: spiralState.rooms,\n characters: [...spiralState.characters.values()]\n };\n}\n\nfunction broadcastSpiralState() {\n broadcast(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload()));\n}\n\nfunction ensureRoom(roomId, fallbackName = null) {\n if (!roomId) return spiralState.rooms[0].id;\n const exists = spiralState.rooms.find((r) => r.id === roomId);\n if (exists) return exists.id;\n const created = { id: roomId, name: fallbackName || `Spiral-${roomId.slice(-4)}` };\n spiralState.rooms.push(created);\n return created.id;\n}\n\nfunction syncSpiralCharactersWithWorld(snapshot) {\n const agents = snapshot?.agents || [];\n for (const a of agents) {\n if (!spiralState.characters.has(a.id)) {\n spiralState.characters.set(a.id, {\n id: a.id,\n name: a.id,\n role: \"agent\",\n roomId: spiralState.rooms[0].id,\n source: \"world\"\n });\n }\n }\n}\n\nfunction broadcastExcept(msgObj, exceptSocket) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client !== exceptSocket && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction validateInputFrame(payload, socket) {\n const stats = clientStats.get(socket.clientId) || { lastFrame: 0, windowStart: Date.now(), msgCount: 0 };\n const now = Date.now();\n if (now - stats.windowStart > 5000) {\n stats.windowStart = now;\n stats.msgCount = 0;\n }\n stats.msgCount += 1;\n clientStats.set(socket.clientId, stats);\n if (stats.msgCount > 120) return \"Rate limit exceeded.\";\n\n if (!payload || !Number.isInteger(payload.frame)) return \"frame must be integer.\";\n if (payload.frame <= stats.lastFrame) return \"frame must be monotonic.\";\n const commands = Array.isArray(payload.commands) ? payload.commands : [];\n if (commands.length > 8) return \"Too many commands in one frame.\";\n for (const cmd of commands) {\n if (!cmd || typeof cmd.command !== \"string\") return \"Invalid command format.\";\n if (\n cmd.command !== \"SPAWN_AGENT\" &&\n cmd.command !== \"LIST_CASTLES\" &&\n cmd.command !== \"SPAWN_ENTITY\" &&\n cmd.command !== \"CREATE_CASTLE\"\n ) return \"Unsupported command.\";\n }\n stats.lastFrame = payload.frame;\n clientStats.set(socket.clientId, stats);\n return null;\n}\n\nfunction validateSignalPayload(payload) {\n if (!payload || typeof payload !== \"object\") return \"Invalid signaling payload.\";\n const signalType = payload.signalType;\n if (![\"OFFER\", \"ANSWER\", \"ICE\"].includes(signalType)) return \"Invalid signal type.\";\n if (typeof payload.to !== \"string\" || payload.to.length < 3) return \"Invalid signal destination.\";\n if (signalType !== \"ICE\" && JSON.stringify(payload.sdp || {}).length > 16 * 1024) return \"SDP too large.\";\n if (signalType === \"ICE\" && JSON.stringify(payload.candidate || {}).length > 6 * 1024) return \"ICE too large.\";\n return null;\n}\n\nfunction sanitizeRawMessage(raw) {\n if (!raw) return null;\n const text = raw.toString();\n if (Buffer.byteLength(text, \"utf8\") > MAX_MESSAGE_BYTES) return null;\n return text;\n}\n\nwss.on(\"connection\", async (socket, req) => {\n const origin = req?.headers?.origin || \"\";\n if (ALLOWED_ORIGINS.length && !ALLOWED_ORIGINS.includes(origin)) {\n socket.close(1008, \"Origin not allowed\");\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const path = req?.url || \"\";\n const query = new URL(`ws://localhost${path}`).searchParams;\n const token = query.get(\"token\");\n if (!token || token !== REQUIRED_GATEWAY_TOKEN) {\n socket.close(1008, \"Invalid gateway token\");\n return;\n }\n }\n if (REQUIRE_AUTH) {\n const result = await verifyClientToken(req);\n if (!result.ok) {\n const isLocal = origin.includes(\"localhost\") || origin.includes(\"127.0.0.1\") || !origin;\n if (!(ALLOW_DEV_ANON && isLocal)) {\n socket.close(1008, result.reason);\n return;\n }\n socket.auth = { ok: true, kind: \"dev-anon\", user: { uid: \"dev-anon\" } };\n } else {\n socket.auth = result;\n }\n }\n\n socket.clientId = `c-${Math.random().toString(36).slice(2, 8)}`;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.HELLO, { message: \"Castle Gateway online.\", clientId: socket.clientId })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.WORLD_SNAPSHOT, orchestrator.worldSnapshot())));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.BROADCAST_STATE, { broadcasterClientId, viewers: [...wss.clients].filter((c) => c.readyState === 1).length })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload())));\n broadcastExcept(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }), socket);\n\n socket.on(\"message\", (raw) => {\n const safeRaw = sanitizeRawMessage(raw);\n if (!safeRaw) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Message too large or invalid.\" })));\n return;\n }\n const parsed = safeJsonParse(safeRaw);\n if (!parsed?.type) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid message.\" })));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND_TEXT) {\n const canonical = parseTextToCommand(parsed.payload?.text);\n if (!canonical) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Unknown command text.\" })));\n return;\n }\n const result = orchestrator.applyCommand(canonical);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND) {\n const result = orchestrator.applyCommand(parsed.payload);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n }\n\n if (parsed.type === WS_MESSAGE.INPUT_FRAME) {\n const err = validateInputFrame(parsed.payload, socket);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const frame = parsed.payload.frame;\n const commands = Array.isArray(parsed.payload?.commands) ? parsed.payload.commands : [];\n orchestrator.queueInputFrame(socket.clientId, frame, commands);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, queued: true, frame })));\n }\n\n if (parsed.type === WS_MESSAGE.SIGNAL) {\n const payload = parsed.payload || {};\n const err = validateSignalPayload(payload);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const signalType = payload.signalType;\n const to = payload.to;\n for (const client of wss.clients) {\n if (client.clientId === to && client.readyState === 1) {\n client.send(JSON.stringify(createEnvelope(WS_MESSAGE.SIGNAL, { ...payload, from: socket.clientId })));\n break;\n }\n }\n }\n\n if (parsed.type === WS_MESSAGE.DRONE_TELEMETRY) {\n const payload = parsed.payload || {};\n const json = JSON.stringify(payload);\n if (Buffer.byteLength(json, \"utf8\") > 48 * 1024) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"DRONE_TELEMETRY payload too large.\" })));\n return;\n }\n broadcastExcept(createEnvelope(WS_MESSAGE.DRONE_TELEMETRY, { ...payload, relayFrom: socket.clientId }), socket);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_SOCIAL_PULSE) {\n handleCastleSocialPulse(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_WAL_PEER_FEED) {\n handleCastleWalPeerFeed(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.OPEN_DATA_QUERY) {\n queryOpenData(parsed.payload)\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: true, result })));\n })\n .catch((e) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: false, error: e.message })));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_QUERY) {\n runRhizohBrainV2({\n ...(parsed.payload || {}),\n uid: socket?.auth?.user?.uid || \"unknown\"\n })\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, result)));\n })\n .catch(async () => {\n const fallback = await runRhizohBrain(parsed.payload || {}).catch(() => ({\n ok: false,\n reply: \"Rhizoh temporary disturbance. Habitat remains stable.\"\n }));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, fallback)));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.BROADCAST_REGISTER) {\n const role = String(parsed.payload?.role || \"\");\n if (role === \"GENESIS_BROADCAST_AGENT\") {\n broadcasterClientId = socket.clientId;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, registered: role })));\n broadcastState();\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.STUDIO_CUE) {\n // Only registered broadcaster can push global cues.\n if (!broadcasterClientId || socket.clientId !== broadcasterClientId) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Only broadcaster agent can emit STUDIO_CUE.\" })));\n return;\n }\n const cue = parsed.payload || {};\n if (typeof cue !== \"object\") {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid STUDIO_CUE payload.\" })));\n return;\n }\n broadcast(createEnvelope(WS_MESSAGE.STUDIO_CUE, cue));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_JOIN_ROOM) {\n const requested = String(parsed.payload?.roomId || \"\").trim();\n const requestedName = String(parsed.payload?.roomName || \"\").trim();\n const roomId = ensureRoom(requested || `room-${Math.random().toString(36).slice(2, 7)}`, requestedName || null);\n spiralState.activeByClient.set(socket.clientId, roomId);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, roomId })));\n broadcastSpiralState();\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_CREATE_CHARACTER) {\n const name = String(parsed.payload?.name || \"\").trim().slice(0, 32) || `AI-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;\n const role = String(parsed.payload?.role || \"agent\").trim().slice(0, 24) || \"agent\";\n const roomId = ensureRoom(\n String(parsed.payload?.roomId || spiralState.activeByClient.get(socket.clientId) || spiralState.rooms[0].id),\n null\n );\n\n // Character creation always maps to an authoritative world agent.\n const result = orchestrator.applyCommand({\n command: COMMAND.SPAWN_AGENT,\n params: { targetCastleId: \"castle-01\" }\n });\n if (!result?.ok || !result?.data?.id) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Character creation failed.\" })));\n return;\n }\n const id = result.data.id;\n spiralState.characters.set(id, { id, name, role, roomId, source: \"spawned\" });\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, characterId: id, roomId })));\n broadcastSpiralState();\n return;\n }\n });\n\n socket.on(\"close\", () => {\n if (socket.clientId === broadcasterClientId) broadcasterClientId = null;\n clientStats.delete(socket.clientId);\n spiralState.activeByClient.delete(socket.clientId);\n removeCastleSocialClientFromAllRooms(socket.clientId);\n broadcast(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }));\n broadcastState();\n });\n});\n\nsetInterval(() => {\n void (async () => {\n try {\n const { frame, snapshot, delta, packedDelta } = orchestrator.tick(1 / 20);\n syncSpiralCharactersWithWorld(snapshot);\n broadcast(createEnvelope(WS_MESSAGE.NETWORK_TICK, { frame }));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_TICK, snapshot));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA, delta));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA_PACKED, packedDelta));\n broadcastSpiralState();\n await runAcademyEventTick();\n const executed = await tickAutomations(Date.now());\n for (const a of executed) {\n await addTranscript(a.uid, {\n source: \"planner\",\n eventType: \"automation-executed\",\n text: `Automation executed: ${a.title} (${a.action})`,\n roomId: \"studio-main\",\n meta: { automationId: a.id, action: a.action, deviceId: a.deviceId || \"\" }\n });\n }\n } catch (e) {\n console.error(\"[GATEWAY] world tick / automations loop error:\", String(e?.message || e));\n }\n })();\n}, 50);\n\n(async () => {\n await rhizohRuntime.telemetry.initOpenTelemetry();\n if (String(process.env.CASTLE_GENESIS_DISK_PERSIST ?? \"\").trim() === \"\") {\n process.env.CASTLE_GENESIS_DISK_PERSIST = \"1\";\n }\n const genesisDiskBoot = await hydrateGenesisContinuityPersistenceBootV0();\n if (!genesisDiskBoot.ok) {\n console.warn(\"[GATEWAY] genesis disk hydrate failed; continuing with in-memory genesis continuity\");\n }\n httpServer.listen(PORT, () => {\n startGenesisCanonicalClock();\n installGenesisCheckpointSurfaceGetter(buildGenesisRuntimeSurfacePayloadLive);\n setGenesisContinuityAfterPublishHook((seq) => {\n noteGenesisCheckpointSeqCommitted(seq);\n noteGenesisContinuitySeqAcceptedV0(seq);\n });\n startGenesisContinuityInfraSampler(buildGenesisRuntimeSurfacePayloadLive, 2000);\n logProductionObservatorySurfaceGuardsV0();\n logGatewaySubstrateAuthorityGuardsV0();\n console.log(`[GATEWAY] ws/http://localhost:${PORT}`);\n console.log(\n `[GATEWAY] genesis continuity: GET ${rhizohRuntime.routes.genesisRuntime} | POST ${rhizohRuntime.routes.genesisIngress} | SSE ${rhizohRuntime.routes.genesisStream} | replay ${rhizohRuntime.routes.genesisReplay} | diff ${rhizohRuntime.routes.genesisReplayDiff} | equiv ${rhizohRuntime.routes.genesisReplayEquivalence} | analytics ${rhizohRuntime.routes.genesisReplayAnalytics} | evolution ${rhizohRuntime.routes.genesisReplayEvolution}`\n );\n if (String(process.env.CASTLE_GENESIS_OBSERVATORY_LIVE_V01 ?? \"\").trim() === \"1\") {\n console.log(\"GENESIS OBSERVATORY LIVE v0.1 — READ ONLY PHASE STARTED\");\n }\n });\n})().catch((e) => {\n console.error(\"[GATEWAY] fatal boot (before or during listen):\", e);\n process.exit(1);\n});\n"}, "ac0797d8abd8828797b3da31c95e94c9e0944701": {"t": 1785754171.9484131, "n": 151002, "h": "c7cecc7b0c54200d", "c": "import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport dotenv from \"dotenv\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconsole.log(\"🔥 [GENESIS_BOOT] entry file:\", __filename);\nconsole.log(\"🔥 [GENESIS_BOOT] node env:\", process.env.NODE_ENV);\nconsole.log(\"🔥 [GENESIS_BOOT] pid:\", process.pid);\nconsole.log(\"🧬 [GENESIS_BOOT] cwd:\", process.cwd());\nconsole.log(\"🧬 [GENESIS_BOOT] import.meta.url:\", import.meta.url);\ndotenv.config({ path: path.join(__dirname, \"..\", \".env\") });\ndotenv.config({ path: path.join(__dirname, \"..\", \".env.local\"), override: true });\n\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport { createServer } from \"node:http\";\nimport { createOrchestrator } from \"@castle/orchestrator\";\nimport { parseTextToCommand } from \"@castle/command-dsl\";\nimport { WS_MESSAGE, COMMAND, createEnvelope, safeJsonParse } from \"@castle/protocol\";\nimport { queryOpenData } from \"./openData.js\";\nimport { verifyClientToken } from \"./auth.js\";\nimport { runRhizohBrain } from \"./rhizohBrain.js\";\nimport { queryRhizohLlm } from \"./rhizohLlmGateway.js\";\nimport { rhizohGatewayTurn } from \"./rhizohGatewayTurn.js\";\nimport {\n meshAppendDelta,\n meshContinuityAggregate,\n meshJoin,\n meshLeave,\n meshReplayFromNodeId,\n meshReplayFromSeq,\n meshSnapshot,\n meshSubscribeSse\n} from \"./presenceRoomMesh.js\";\nimport { runRhizohBrainV2 } from \"./rhizohBrainV2.js\";\nimport {\n listConnections,\n createConnection,\n updateConnection,\n deleteConnection,\n setDefaultConnection,\n resolveConnection\n} from \"./llmConnectionsStore.js\";\nimport { checkHttpRateLimit, getHttpClientIp } from \"./castleHttpRateLimit.js\";\nimport {\n submitAbuseReportV0,\n listModerationQueueV0,\n softBlockUserV0,\n removeSoftBlockV0,\n listSoftBlockedUsersV0\n} from \"./ops/moderationMvpV0.js\";\nimport { listRecentAgentSnapshotsV0 } from \"./ops/agentObservabilityV0.js\";\nimport { readAgentContainmentConfigV0 } from \"./ops/agentContainmentV0.js\";\nimport { readCostContainmentConfigV0, getCostContainmentStatsV0 } from \"./ops/costContainmentV0.js\";\nimport { getGlobalCostLedgerSnapshotV0, getGclBackendStatusV0 } from \"./ops/globalCostLedgerV0.js\";\nimport { evaluateEconomicStrategyL3V0 } from \"./ops/economicStrategyEngineL3V0.js\";\nimport { getPhasedRolloutStatsV0, readPhasedRolloutTierV0 } from \"./ops/phasedRolloutV0.js\";\nimport { buildUnifiedStateNarrativeV0 } from \"./ops/unifiedStateNarrativeV0.js\";\nimport { resolveHardeningTenantIdV0 } from \"./ops/narrativeTenantIsolationV0.js\";\nimport {\n classifyStressResponseV0,\n verifyStressTaxonomyCoverageV0,\n verifyStressConflictResolutionV0\n} from \"./ops/stressResponseTaxonomyV0.js\";\nimport { verifyResolutionStabilityEnvelopeV0 } from \"./ops/resolutionStabilityEnvelopeV0.js\";\nimport { logLlmAccess } from \"./castleLlmAudit.js\";\nimport { appendMemory, listMemories, getMemoryContext, getPersonaGoalMemory, setPersonaGoalMemory, autoCompactMemories } from \"./memoryStore.js\";\nimport { getFirebasePersistence } from \"./firebasePersistence.js\";\nimport { registerAgentIdentity, listAgentIdentities, getAgentIdentity, updateAgentIdentity } from \"./agentIdentityStore.js\";\nimport { queueAcademyEvent, listAcademyEvents, resolveAcademyEvent, runAcademyEventTick } from \"./academyEventEngine.js\";\nimport { enqueuePdfJob, getPdfJob } from \"./pdfPipeline.js\";\nimport {\n createPublishSession,\n listPublishSessions,\n updatePublishSession,\n addTranscript,\n listTranscripts,\n searchMetadata,\n registerDevice,\n listDevices,\n updateDevice,\n appendTelemetry,\n listTelemetry,\n createAutomation,\n listAutomations,\n tickAutomations,\n enqueueCommand,\n listCommands,\n updateCommandStatus,\n registerSocialChannel,\n listSocialChannels\n} from \"./studioOpsStore.js\";\nimport { metrics as infraMetrics } from \"./infra/metrics.js\";\nimport { renderRhizohPrometheusMetrics, rhizohEnterpriseMetrics } from \"./infra/rhizohEnterpriseMetrics.js\";\nimport { scoreHealth } from \"./infra/healthScore.js\";\nimport { getTraceById, listTracesBySession } from \"./infra/traceRegistry.js\";\nimport { canonicalEpistemicSealString, hashAndSignEpistemicSeal, EPISTEMIC_SEAL_SCHEMA } from \"./epistemicSeal.js\";\nimport { persistEpistemicLedgerBatch } from \"./epistemicLedgerStore.js\";\nimport { persistEpistemicForecastBatch } from \"./epistemicForecastStore.js\";\nimport { buildRhizohExternalGroundTruthPayload } from \"./rhizohExternalGroundTruthGateway.js\";\nimport { ingestRhizohExternalLossBatchHttp } from \"./rhizohExternalLossIngestGateway.js\";\nimport {\n ingestRhizohProductOutcomeHttp,\n listRhizohProductOutcomeAggregates,\n verifyRhizohOutcomeHmac,\n verifyRhizohOutcomeSourceToken\n} from \"./rhizohProductOutcomeIngestGateway.js\";\nimport { initRhizoh } from \"./rhizohProductionBootstrap.js\";\nimport {\n buildGenesisRuntimeSurfacePayload,\n recordGenesisEpistemicLedgerPersisted,\n recordGenesisEpistemicSealIssued,\n startGenesisCanonicalClock\n} from \"./genesisRuntimeSurfaceV0.js\";\nimport { registerGenesisSseClient, getGenesisContinuitySeq, setGenesisContinuityAfterPublishHook } from \"./genesisContinuityStreamHubV0.js\";\nimport { computeGenesisReplayFingerprintV0 } from \"./genesisReplayFingerprintV0.js\";\nimport { buildGenesisGatewayCapabilitiesV0 } from \"./genesisGatewayCapabilitiesV0.js\";\nimport {\n installGenesisCheckpointSurfaceGetter,\n noteGenesisCheckpointSeqCommitted,\n getLatestGenesisSignedCheckpoint\n} from \"./genesisContinuityCheckpointV0.js\";\nimport { hydrateGenesisContinuityPersistenceBootV0 } from \"./genesisContinuityHydrateBootV0.js\";\nimport { startGenesisContinuityInfraSampler } from \"./genesisContinuityInfraSamplerV0.js\";\nimport {\n genesisCheckpointQueryBySeqV0,\n genesisCheckpointQueryRangeV0,\n genesisCheckpointQueryLineageV0\n} from \"./genesisCheckpointQueryV0.js\";\nimport {\n genesisContinuityEventArchiveEnabled,\n queryGenesisContinuityEventArchiveV0,\n GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION\n} from \"./genesisContinuityEventArchiveV0.js\";\nimport { resolveGenesisReplayRouterV1, GENESIS_REPLAY_ROUTER_SCHEMA } from \"./genesisReplayRouterV1.js\";\nimport {\n computeGenesisReplayTemporalDiffV1,\n GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA\n} from \"./genesisReplayTemporalDiffV1.js\";\nimport { compareGenesisReplayEquivalenceV1 } from \"./genesisReplayEquivalenceV1.js\";\nimport { computeGenesisReplayAnalyticsV1 } from \"./genesisReplayAnalyticsV1.js\";\nimport { computeGenesisReplayEvolutionV1 } from \"./genesisReplayEvolutionV1.js\";\nimport { handleStripeCheckoutCreate, handleStripeMembershipWebhook } from \"./stripeWebhookMembership.js\";\nimport { buildRealityHealthPayload } from \"./edgeRealityConsistencyV1.js\";\nimport {\n assertWalPeerSocketAuthorityV0,\n buildGatewaySubstrateAuthoritySnapshotV0,\n logGatewaySubstrateAuthorityGuardsV0,\n validateWalPeerFeedSignatureV0\n} from \"./gatewaySubstrateAuthorityV0.js\";\nimport {\n buildSubstrateOperationalSnapshotV0,\n recordWalPeerRejectV0,\n renderSubstratePrometheusMetricsV0\n} from \"./infra/substrateOperationalMetrics.js\";\nimport { ingestClientSubstrateHealthV0 } from \"./rhizohSubstrateHealthIngestV0.js\";\nimport { recordSubstrateOtelEventV0 } from \"./infra/opentelemetrySubstrateV0.js\";\nimport {\n GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n getGenesisWorldObservationIngressStatusV0,\n ingestGenesisWorldObservationV0\n} from \"./genesisContinuityClientIngressV0.js\";\nimport {\n getGenesisContinuitySeqAuditV0,\n noteGenesisContinuitySeqAcceptedV0\n} from \"./genesisContinuitySeqGapV0.js\";\n\n/** Render / Fly / Railway: `PORT` — yoksa `CASTLE_GATEWAY_PORT` — yoksa 8090. */\nconst PORT =\n Number(process.env.PORT) ||\n Number(process.env.CASTLE_GATEWAY_PORT) ||\n 8090;\n/** Spiral / world WebSocket server — set after `wss` init; used by genesis runtime surface. */\nlet spiralWssForGenesis = null;\nconst MAX_MESSAGE_BYTES = Number(process.env.CASTLE_MAX_MESSAGE_BYTES || 32 * 1024);\nconst REQUIRED_GATEWAY_TOKEN = process.env.CASTLE_GATEWAY_TOKEN || \"\";\nconst ALLOWED_ORIGINS = (process.env.CASTLE_ALLOWED_ORIGINS || \"\")\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n\nfunction normalizeHttpOrigin(origin) {\n return String(origin || \"\")\n .trim()\n .replace(/\\/+$/, \"\");\n}\n\n/** HTTP CORS: `CASTLE_ALLOWED_ORIGINS` + `CASTLE_HTTP_CORS_ORIGIN` (tek origin); `CASTLE_HTTP_CORS_ORIGIN=*` → wildcard. */\nfunction buildHttpCorsOriginAllowSet() {\n const set = new Set();\n for (const o of ALLOWED_ORIGINS) {\n const n = normalizeHttpOrigin(o);\n if (n) set.add(n);\n }\n const primary = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n if (primary && primary !== \"*\") set.add(primary);\n return set;\n}\n\nconst HTTP_CORS_ORIGIN_ALLOW_SET = buildHttpCorsOriginAllowSet();\n\n/**\n * Tarayıcı `Origin` ile tam eşleşme ister. Tek sabit `CASTLE_HTTP_CORS_ORIGIN` yeterli değil:\n * örn. `web.app` vs `firebaseapp.com` veya Render’da yanlış/eksik env.\n * @returns {string|null} `Access-Control-Allow-Origin` değeri; null = başlık yok (istemci CORS fail).\n */\nfunction accessControlAllowOriginValue(req) {\n const primaryEnv = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n if (primaryEnv === \"*\") return \"*\";\n const reqOrigin = normalizeHttpOrigin(req.headers?.origin);\n if (HTTP_CORS_ORIGIN_ALLOW_SET.size === 0) return \"*\";\n if (!reqOrigin) return \"*\";\n if (HTTP_CORS_ORIGIN_ALLOW_SET.has(reqOrigin)) return reqOrigin;\n return null;\n}\n\nfunction applyHttpCorsHeaders(req, res) {\n const allow = accessControlAllowOriginValue(req);\n if (allow) {\n res.setHeader(\"Access-Control-Allow-Origin\", allow);\n if (allow !== \"*\") res.setHeader(\"Vary\", \"Origin\");\n }\n res.setHeader(\"Access-Control-Allow-Methods\", \"GET,POST,OPTIONS\");\n res.setHeader(\n \"Access-Control-Allow-Headers\",\n \"Content-Type, Authorization, X-Castle-Dev-Uid, X-Castle-Guest-Id, X-Castle-Gateway-Token, X-Rhizoh-Outcome-Signature, X-Rhizoh-Outcome-Source-Token\"\n );\n}\nconst REQUIRE_AUTH = process.env.CASTLE_REQUIRE_AUTH === \"true\";\nconst ALLOW_DEV_ANON = process.env.CASTLE_ALLOW_DEV_ANON !== \"false\";\nconst ALLOW_DEV_HTTP_UID = process.env.CASTLE_ALLOW_DEV_HTTP_UID !== \"false\";\n/** Presence mesh: allow `X-Castle-Guest-Id` when Firebase auth absent (default on). Set `false` to require auth only. */\nconst ALLOW_MESH_GUEST = process.env.CASTLE_ALLOW_MESH_GUEST !== \"false\";\nconst SOCIAL_RETRY_MAX = Math.max(0, Math.min(5, Number(process.env.CASTLE_SOCIAL_RETRY_MAX || 2)));\nconst SOCIAL_RETRY_BASE_MS = Math.max(100, Number(process.env.CASTLE_SOCIAL_RETRY_BASE_MS || 700));\nconst TELEGRAM_BOT_TOKEN = process.env.CASTLE_TELEGRAM_BOT_TOKEN || \"\";\nconst WHATSAPP_TOKEN = process.env.CASTLE_WHATSAPP_TOKEN || \"\";\nconst WHATSAPP_PHONE_NUMBER_ID = process.env.CASTLE_WHATSAPP_PHONE_NUMBER_ID || \"\";\nconst RL_RHIZOH_LLM_PER_MIN = Math.max(5, Number(process.env.CASTLE_RL_RHIZOH_LLM_PER_MIN || 40));\nconst RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN || 120));\nconst RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN = Math.max(8, Number(process.env.CASTLE_RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN || 48));\nconst RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN = Math.max(4, Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN || 24));\nconst RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN = Math.max(\n 4,\n Number(process.env.CASTLE_RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN || 18)\n);\nconst RL_EPISTEMIC_SEAL_PER_MIN = Math.max(10, Number(process.env.CASTLE_RL_EPISTEMIC_SEAL_PER_MIN || 120));\nconst RL_EPISTEMIC_LOG_PER_MIN = Math.max(20, Number(process.env.CASTLE_RL_EPISTEMIC_LOG_PER_MIN || 240));\nconst EPISTEMIC_SEAL_SECRET = String(\n process.env.CASTLE_EPISTEMIC_SEAL_SECRET || process.env.CASTLE_GATEWAY_TOKEN || \"\"\n).trim();\nconst RL_LLM_CONN_TEST_PER_MIN = Math.max(2, Number(process.env.CASTLE_RL_LLM_CONNECTION_TEST_PER_MIN || 8));\n\n/** One-shot production warnings: CORS surface, auth toggle, dev bypass flags (observatory deploy hygiene). */\nfunction logProductionObservatorySurfaceGuardsV0() {\n if (String(process.env.NODE_ENV || \"\").toLowerCase() !== \"production\") return;\n const tag = \"[GATEWAY_PROD_GUARD]\";\n const primaryCors = normalizeHttpOrigin(process.env.CASTLE_HTTP_CORS_ORIGIN);\n const corsListEmpty = ALLOWED_ORIGINS.length === 0;\n if (corsListEmpty && !primaryCors) {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty and CASTLE_HTTP_CORS_ORIGIN empty — browser SSE/replay may fail; set explicit Firebase Hosting origins`\n );\n } else if (corsListEmpty && primaryCors && primaryCors !== \"*\") {\n console.warn(\n `${tag} CORS: CASTLE_ALLOWED_ORIGINS empty — only CASTLE_HTTP_CORS_ORIGIN set; add web.app + firebaseapp.com (and previews) to CASTLE_ALLOWED_ORIGINS if clients use alternate hostnames`\n );\n }\n if (primaryCors && /localhost|127\\.0\\.0\\.1/i.test(primaryCors)) {\n console.warn(`${tag} CORS: CASTLE_HTTP_CORS_ORIGIN is localhost while NODE_ENV=production`);\n }\n if (REQUIRE_AUTH) {\n console.warn(\n `${tag} AUTH: CASTLE_REQUIRE_AUTH=true — clients must send a valid gateway credential (otherwise silent 401 on replay/SSE); EventSource cannot set custom headers unless proxied`\n );\n if (REQUIRED_GATEWAY_TOKEN.length < 16) {\n console.warn(`${tag} AUTH: CASTLE_GATEWAY_TOKEN missing or short while require-auth enabled`);\n }\n }\n if (ALLOW_DEV_ANON) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_ANON is not \"false\" — dev anonymous path may be live in production (set CASTLE_ALLOW_DEV_ANON=false)`\n );\n }\n if (ALLOW_DEV_HTTP_UID) {\n console.warn(\n `${tag} DEV: CASTLE_ALLOW_DEV_HTTP_UID is not \"false\" — X-Castle-Dev-Uid bypass risk (set CASTLE_ALLOW_DEV_HTTP_UID=false)`\n );\n }\n}\n\nconst rhizohRuntime = initRhizoh();\n\n/** İstemci: llmKeySource veya keySource — env | user_connection | auto */\nfunction normalizeClientLlmKeySource(raw) {\n const s = String(raw ?? \"\").trim().toLowerCase();\n if (s === \"env\" || s === \"server\") return \"env\";\n if (s === \"user_connection\" || s === \"connection\" || s === \"user\") return \"user_connection\";\n if (s === \"auto\" || s === \"\") return \"auto\";\n return null;\n}\n\nfunction sendJson(res, statusCode, payload) {\n res.writeHead(statusCode, { \"Content-Type\": \"application/json; charset=utf-8\" });\n res.end(JSON.stringify(payload));\n}\n\nfunction logRhizohHealth(stage, detail = {}) {\n try {\n console.info(`[CASTLE_${String(stage || \"unknown\")}]`, detail && typeof detail === \"object\" ? detail : {});\n } catch {\n /* noop */\n }\n}\n\nfunction renderInfraMetricsProm() {\n const lines = [\n `castle_gateway_events_processed_total ${infraMetrics.eventsProcessed}`,\n `castle_gateway_duplicate_reject_total ${infraMetrics.duplicateRejects}`,\n `castle_gateway_enqueue_latency_ms ${infraMetrics.enqueueLatencyMs}`,\n `castle_gateway_queue_depth ${infraMetrics.queueDepth}`,\n `castle_gateway_queue_lag_seconds ${(Number(infraMetrics.queueLag || 0) / 1000).toFixed(3)}`,\n `castle_gateway_errors_total ${infraMetrics.errors}`\n ];\n const buckets = infraMetrics.enqueueLatencyBuckets || {};\n for (const k of Object.keys(buckets)) {\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"${k}\"} ${buckets[k]}`);\n }\n lines.push(`castle_gateway_enqueue_latency_bucket{le=\"+Inf\"} ${infraMetrics.eventsProcessed}`);\n if (process.env.CASTLE_SUBSTRATE_METRICS !== \"0\") {\n lines.push(renderSubstratePrometheusMetricsV0().trimEnd());\n }\n return lines.join(\"\\n\");\n}\n\nasync function fetchJsonWithTimeout(url, timeoutMs = 1200) {\n const ctl = new AbortController();\n const tid = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const res = await fetch(url, { signal: ctl.signal });\n if (!res.ok) return null;\n return await res.json();\n } catch {\n return null;\n } finally {\n clearTimeout(tid);\n }\n}\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction getHttpPathname(req) {\n let p = String(req.url || \"\").split(\"?\")[0];\n /* Reverse proxies / clients sometimes append `/` — avoid silent 404 on e.g. `/rhizoh/genesis/stream/`. */\n while (p.length > 1 && p.endsWith(\"/\")) {\n p = p.slice(0, -1);\n }\n return p;\n}\n\nfunction rhizohLlmEnvConfigured() {\n const p = String(process.env.CASTLE_LLM_PROVIDER || \"openai\").toLowerCase();\n const key =\n p === \"anthropic\"\n ? process.env.ANTHROPIC_API_KEY\n : p === \"gemini\"\n ? process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY\n : p === \"xai\"\n ? process.env.XAI_API_KEY\n : p === \"deepseek\"\n ? process.env.DEEPSEEK_API_KEY\n : p === \"mistral\"\n ? process.env.MISTRAL_API_KEY\n : p === \"openrouter\"\n ? process.env.OPENROUTER_API_KEY\n : process.env.OPENAI_API_KEY;\n return !!String(key || \"\").trim();\n}\n\n/**\n * @param {import(\"firebase-admin/firestore\").Firestore | null} db\n * @param {number} timeoutMs\n */\nasync function probeFirestoreReachable(db, timeoutMs = 2000) {\n if (!db) return { ok: false, reason: \"no_db\" };\n const timeout = sleep(timeoutMs).then(() => ({ ok: false, reason: \"timeout\" }));\n const work = db\n .collection(\"_castle_gateway_health\")\n .doc(\"ping\")\n .get()\n .then(() => ({ ok: true, reason: \"reachable\" }))\n .catch((e) => ({ ok: false, reason: String(e?.message || e || \"firestore_error\") }));\n return Promise.race([work, timeout]);\n}\n\nfunction stripHtml(text = \"\") {\n return String(text)\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \" \")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \" \")\n .replace(/<[^>]+>/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction haversineMeters(aLat, aLon, bLat, bLon) {\n const R = 6371000;\n const dLat = ((bLat - aLat) * Math.PI) / 180;\n const dLon = ((bLon - aLon) * Math.PI) / 180;\n const sa = Math.sin(dLat / 2);\n const sb = Math.sin(dLon / 2);\n const x = sa * sa + Math.cos((aLat * Math.PI) / 180) * Math.cos((bLat * Math.PI) / 180) * sb * sb;\n return 2 * R * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));\n}\n\nasync function checkPublishTargetHealth(protocol, target) {\n const p = String(protocol || \"\").toUpperCase();\n const t = String(target || \"\").trim();\n if (!t) return { ok: false, reason: \"target_missing\", shouldReconnect: false };\n if (p === \"RTMP\") {\n const ok = t.startsWith(\"rtmp://\") || t.startsWith(\"rtmps://\");\n return { ok, reason: ok ? \"rtmp_target_format_ok\" : \"invalid_rtmp_target\", shouldReconnect: !ok, reconnectInSec: 15 };\n }\n if (p === \"WHIP\" || p === \"SFU\") {\n try {\n const res = await fetch(t, { method: \"OPTIONS\" });\n const ok = res.status < 500;\n return { ok, reason: `http_${res.status}`, shouldReconnect: !ok, reconnectInSec: 8 };\n } catch {\n return { ok: false, reason: \"network_unreachable\", shouldReconnect: true, reconnectInSec: 8 };\n }\n }\n return { ok: true, reason: \"unchecked_protocol\", shouldReconnect: false };\n}\n\nasync function fetchTextFromUrl(url) {\n const res = await fetch(url, { method: \"GET\" });\n if (!res.ok) throw new Error(`fetch_${res.status}`);\n const ct = String(res.headers.get(\"content-type\") || \"\");\n if (ct.includes(\"application/pdf\")) throw new Error(\"pdf_binary_not_supported_directly\");\n const body = await res.text();\n return stripHtml(body).slice(0, 14000);\n}\n\nfunction buildTaskActionGraph(task = \"\", context = {}) {\n const t = String(task || \"\").toLowerCase();\n if (t.includes(\"dinner\") || t.includes(\"akşam yemeği\") || t.includes(\"aksam yemegi\")) {\n return {\n taskTitle: \"Dinner Prep Planner\",\n nodes: [\n { id: \"n1\", title: \"Kitchen preheat\", action: \"smart-oven.preheat\", params: { temperatureC: 190, mode: \"fan\" } },\n { id: \"n2\", title: \"Lighting comfort\", action: \"smart-light.set_scene\", params: { scene: \"warm_dinner\" } },\n { id: \"n3\", title: \"Fridge inventory check\", action: \"smart-fridge.inventory_scan\", params: {} },\n { id: \"n4\", title: \"Reminder notification\", action: \"smart-watch.notify\", params: { text: \"Dinner prep started.\" } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" },\n { from: \"n3\", to: \"n4\" }\n ]\n };\n }\n return {\n taskTitle: task || \"General Planner\",\n nodes: [\n { id: \"n1\", title: \"Safety check\", action: \"device.safety_check\", params: { context } },\n { id: \"n2\", title: \"Primary action\", action: \"device.execute_task\", params: { task } },\n { id: \"n3\", title: \"Report\", action: \"studio.timeline_note\", params: { task } }\n ],\n edges: [\n { from: \"n1\", to: \"n2\" },\n { from: \"n2\", to: \"n3\" }\n ]\n };\n}\n\nfunction buildEthicsPrograms() {\n return {\n policyVersion: \"v1\",\n tracks: [\n {\n id: \"elderly-care-assist\",\n audience: \"elderly\",\n title: \"Elderly Assistance Track\",\n principles: [\"human-in-the-loop\", \"consent-first\", \"low-friction alerts\"],\n modules: [\"medication-reminder\", \"fall-risk-check\", \"family-notify\", \"daily-companion-brief\"]\n },\n {\n id: \"kids-safe-learning\",\n audience: \"children\",\n title: \"Kids Safe Learning Track\",\n principles: [\"guardian-consent\", \"age-appropriate-language\", \"content-filtering\"],\n modules: [\"study-coach\", \"screen-time-balance\", \"safe-social-share\", \"emergency-contact\"]\n }\n ]\n };\n}\n\nasync function deliverViaTelegram(channel, text) {\n const chatId = String(channel.endpoint || \"\").trim();\n if (!TELEGRAM_BOT_TOKEN) throw new Error(\"telegram_token_missing\");\n if (!chatId) throw new Error(\"telegram_chat_id_missing\");\n const url = `https://api.telegram.org/bot${encodeURIComponent(TELEGRAM_BOT_TOKEN)}/sendMessage`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ chat_id: chatId, text })\n });\n if (!res.ok) throw new Error(`telegram_http_${res.status}`);\n return { provider: \"telegram\", status: res.status };\n}\n\nasync function deliverViaWhatsapp(channel, text) {\n const to = String(channel.endpoint || \"\").trim();\n if (!WHATSAPP_TOKEN) throw new Error(\"whatsapp_token_missing\");\n if (!WHATSAPP_PHONE_NUMBER_ID) throw new Error(\"whatsapp_phone_number_id_missing\");\n if (!to) throw new Error(\"whatsapp_to_missing\");\n const url = `https://graph.facebook.com/v22.0/${encodeURIComponent(WHATSAPP_PHONE_NUMBER_ID)}/messages`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${WHATSAPP_TOKEN}` },\n body: JSON.stringify({ messaging_product: \"whatsapp\", to, type: \"text\", text: { body: text } })\n });\n if (!res.ok) throw new Error(`whatsapp_http_${res.status}`);\n return { provider: \"whatsapp\", status: res.status };\n}\n\nasync function deliverViaWebhook(channel, text) {\n const endpoint = String(channel.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"webhook_endpoint_missing\");\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text, platform: channel.platform || \"webhook\", ts: Date.now() })\n });\n if (!res.ok) throw new Error(`webhook_http_${res.status}`);\n return { provider: \"webhook\", status: res.status };\n}\n\nasync function deliverSocialMessage(channel, text) {\n const platform = String(channel.platform || \"webhook\").toLowerCase();\n if (platform === \"telegram\") return deliverViaTelegram(channel, text);\n if (platform === \"whatsapp\") return deliverViaWhatsapp(channel, text);\n return deliverViaWebhook(channel, text);\n}\n\nasync function deliverSocialWithRetry(channel, text) {\n let attempt = 0;\n let lastError = null;\n while (attempt <= SOCIAL_RETRY_MAX) {\n try {\n const result = await deliverSocialMessage(channel, text);\n return { ok: true, attempt: attempt + 1, ...result };\n } catch (error) {\n lastError = error;\n if (attempt >= SOCIAL_RETRY_MAX) break;\n const jitter = Math.floor(Math.random() * 90);\n await sleep(SOCIAL_RETRY_BASE_MS * Math.pow(2, attempt) + jitter);\n }\n attempt += 1;\n }\n return { ok: false, attempt: attempt + 1, error: String(lastError?.message || \"delivery_failed\") };\n}\n\nasync function dispatchCommandToAdapter(device, command) {\n const adapter = String(command?.adapter || device?.adapter || \"websocket\").toLowerCase();\n const endpoint = String(device?.endpoint || \"\").trim();\n if (!endpoint) throw new Error(\"device_endpoint_missing\");\n if (adapter === \"mqtt\") {\n // MQTT delivery is proxied via a bridge endpoint (HTTP webhook style).\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n type: \"mqtt-command\",\n topic: `castle/devices/${device.id}/commands`,\n action: command.action,\n params: command.params || {}\n })\n });\n if (!res.ok) throw new Error(`mqtt_bridge_http_${res.status}`);\n return { ok: true, adapter, via: \"http-bridge\", status: res.status };\n }\n if (!endpoint.startsWith(\"ws://\") && !endpoint.startsWith(\"wss://\")) throw new Error(\"invalid_websocket_endpoint\");\n const sendResult = await new Promise((resolve, reject) => {\n const ws = new WebSocket(endpoint);\n const timer = setTimeout(() => {\n try {\n ws.close();\n } catch {\n /* noop */\n }\n reject(new Error(\"websocket_timeout\"));\n }, 3000);\n ws.on(\"open\", () => {\n ws.send(\n JSON.stringify({\n type: \"castle-command\",\n commandId: command.id,\n deviceId: command.deviceId,\n action: command.action,\n params: command.params || {}\n }),\n (err) => {\n clearTimeout(timer);\n ws.close();\n if (err) reject(new Error(\"websocket_send_failed\"));\n else resolve({ ok: true, adapter: \"websocket\", via: \"ws\" });\n }\n );\n });\n ws.on(\"error\", () => {\n clearTimeout(timer);\n reject(new Error(\"websocket_connect_failed\"));\n });\n });\n return sendResult;\n}\n\nfunction readHttpJson(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => {\n if (!buf) return resolve({});\n const payload = safeJsonParse(buf);\n if (!payload || typeof payload !== \"object\") return reject(new Error(\"invalid_json_payload\"));\n resolve(payload);\n });\n req.on(\"error\", reject);\n });\n}\n\n/** Ham gövde (HMAC doğrulaması için). */\nfunction readHttpBodyText(req, limit = 128 * 1024) {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n req.on(\"data\", (chunk) => {\n buf += chunk.toString(\"utf8\");\n if (Buffer.byteLength(buf, \"utf8\") > limit) reject(new Error(\"payload_too_large\"));\n });\n req.on(\"end\", () => resolve(buf));\n req.on(\"error\", reject);\n });\n}\n\nfunction maskOpaqueId(value) {\n const t = String(value || \"\");\n if (!t) return \"(empty)\";\n if (t.length <= 8) return \"****\";\n return `${t.slice(0, 4)}…${t.slice(-2)}`;\n}\n\nfunction readGatewayHttpToken(req) {\n const x = String(req.headers?.[\"x-castle-gateway-token\"] || \"\").trim();\n if (x) return x;\n const auth = String(req.headers?.authorization || \"\");\n const m = /^Bearer\\s+(.+)$/i.exec(auth);\n return m ? String(m[1] || \"\").trim() : \"\";\n}\n\nasync function resolveHttpUser(req) {\n const authResult = await verifyClientToken(req);\n if (authResult?.ok && authResult.user?.uid) {\n return { ok: true, uid: String(authResult.user.uid), mode: authResult.kind || \"auth\" };\n }\n if (ALLOW_DEV_ANON && ALLOW_DEV_HTTP_UID) {\n const devUid = String(req.headers?.[\"x-castle-dev-uid\"] || \"\").trim();\n if (devUid) return { ok: true, uid: `dev-${devUid.slice(0, 64)}`, mode: \"dev-header\" };\n }\n return { ok: false, reason: authResult?.reason || \"auth_required\" };\n}\n\n/** Mesh actor: Firebase uid, dev header, or `guest:<uuid>` via header / `guestId` query (SSE cannot set headers). */\nasync function resolveMeshActor(req) {\n const auth = await resolveHttpUser(req);\n if (auth.ok) return { ok: true, clientUid: auth.uid, mode: auth.mode };\n if (ALLOW_MESH_GUEST) {\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const gid = String(req.headers?.[\"x-castle-guest-id\"] || q.searchParams.get(\"guestId\") || \"\").trim();\n if (gid) {\n const normalized = gid.startsWith(\"guest:\") ? gid.slice(0, 180) : `guest:${gid.slice(0, 160)}`;\n return { ok: true, clientUid: normalized, mode: \"guest-header\" };\n }\n }\n return { ok: false, reason: \"mesh_auth_required\" };\n}\n\n/** Live genesis runtime JSON (GET /rhizoh/genesis/runtime + SSE infra sampler). */\nasync function buildGenesisRuntimeSurfacePayloadLive() {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n let spiralWsActive = 0;\n if (spiralWssForGenesis) {\n for (const c of spiralWssForGenesis.clients) {\n if (c.readyState === 1) spiralWsActive += 1;\n }\n }\n const base = buildGenesisRuntimeSurfacePayload({\n infraMetrics,\n scoredHealth: scored,\n rhizohEnterpriseMetrics,\n mesh: meshContinuityAggregate(),\n workerHealth: worker,\n port: PORT,\n spiralWebSocketClientsActive: spiralWsActive\n });\n return {\n ...base,\n replayFingerprint: computeGenesisReplayFingerprintV0(base),\n gatewayCapabilities: buildGenesisGatewayCapabilitiesV0(),\n genesisStream: {\n schema: \"castle.genesis.stream_cursor.v0\",\n lastAcceptedSeq: getGenesisContinuitySeq(),\n clientIngress: {\n schema: GENESIS_WORLD_OBSERVATION_INGRESS_SCHEMA,\n contract: \"castle.world_observation.ingress_envelope.v1\",\n postPath: rhizohRuntime.routes.genesisIngress,\n closure: getGenesisWorldObservationIngressStatusV0()\n },\n seqContinuity: getGenesisContinuitySeqAuditV0(),\n eventArchive: genesisContinuityEventArchiveEnabled()\n ? {\n enabled: true,\n projection: GENESIS_CONTINUITY_EVENT_ARCHIVE_QUERY_PROJECTION,\n queryPath: rhizohRuntime.routes.genesisContinuityEvents\n }\n : { enabled: false },\n replayRouter: {\n schema: GENESIS_REPLAY_ROUTER_SCHEMA,\n queryPath: rhizohRuntime.routes.genesisReplay,\n queryHint: \"from=&to= or range=from-to; optional type=&checkpoints=0\",\n temporalDiffPath: rhizohRuntime.routes.genesisReplayDiff,\n temporalDiffSchema: GENESIS_REPLAY_TEMPORAL_DIFF_SCHEMA,\n equivalencePath: rhizohRuntime.routes.genesisReplayEquivalence,\n analyticsPath: rhizohRuntime.routes.genesisReplayAnalytics,\n evolutionPath: rhizohRuntime.routes.genesisReplayEvolution\n }\n }\n };\n}\n\nconst httpServer = createServer(async (req, res) => {\n applyHttpCorsHeaders(req, res);\n if (req.method === \"OPTIONS\") {\n res.writeHead(204);\n res.end();\n return;\n }\n\n const pathname = getHttpPathname(req);\n\n if (req.method === \"GET\" && pathname === \"/rhizoh/genesis/__ping\") {\n sendJson(res, 200, {\n ok: true,\n pid: process.pid,\n entry: __filename,\n time: Date.now(),\n genesis: true\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/webhooks/stripe\") {\n await handleStripeMembershipWebhook(req, res);\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/billing/stripe/checkout\") {\n await handleStripeCheckoutCreate(req, res, { readHttpJson, resolveHttpUser, sendJson });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/live\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, live: false, dns: true, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n sendJson(res, 200, { ok: true, live: true, dns: true, service: \"castle-gateway\", ts: Date.now() });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/ready\") {\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, { ok: false, ready: false, phase: \"maintenance\", service: \"castle-gateway\" });\n return;\n }\n try {\n const persistence = getFirebasePersistence();\n const llmProbe = rhizohLlmEnvConfigured();\n sendJson(res, 200, {\n ok: true,\n ready: true,\n dns: true,\n persistence: persistence.mode,\n service: \"castle-gateway\",\n gateway: \"ok\",\n llm: llmProbe ? \"ok\" : \"degraded\",\n spine: llmProbe ? \"ok\" : \"partial\"\n });\n } catch (e) {\n sendJson(res, 503, { ok: false, ready: false, reason: String(e?.message || e), service: \"castle-gateway\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/reality\") {\n const payload = buildRealityHealthPayload();\n sendJson(res, payload.ok ? 200 : 503, payload);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/substrate\") {\n const authority = buildGatewaySubstrateAuthoritySnapshotV0();\n const operational = buildSubstrateOperationalSnapshotV0();\n const drift = buildRealityHealthPayload();\n const ok = Boolean(authority.productionAuthoritySatisfied) && drift.ok;\n sendJson(res, ok ? 200 : 503, {\n ok,\n service: \"castle-gateway\",\n ts: Date.now(),\n authority,\n operational,\n drift: { ok: drift.ok, errors: drift.drift?.errors, warnings: drift.drift?.warnings }\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/substrate/health\") {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const body = await readHttpJson(req);\n const uid = (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestClientSubstrateHealthV0(body, { uid, clientId: uid });\n if (!result.ok) {\n sendJson(res, 400, result);\n return;\n }\n recordSubstrateOtelEventV0(\"client_health_ingest\", { uid: String(uid).slice(0, 32) });\n sendJson(res, 200, result);\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health/deps\") {\n const t0 = Date.now();\n if (process.env.CASTLE_GATEWAY_MAINTENANCE === \"true\") {\n sendJson(res, 503, {\n ok: false,\n dns: true,\n llm: false,\n firestore: false,\n phase: \"maintenance\",\n latencyMs: Date.now() - t0,\n service: \"castle-gateway\"\n });\n return;\n }\n const persistence = getFirebasePersistence();\n let firestoreOk = persistence.mode !== \"firebase\";\n let firestoreDetail = persistence.mode === \"file\" ? \"file_mode\" : \"not_applicable\";\n if (persistence.mode === \"firebase\" && persistence.db) {\n const pr = await probeFirestoreReachable(persistence.db, 2000);\n firestoreOk = pr.ok;\n firestoreDetail = pr.reason || (pr.ok ? \"reachable\" : \"fail\");\n } else if (persistence.mode === \"firebase\" && !persistence.db) {\n firestoreOk = false;\n firestoreDetail = \"firebase_not_initialized\";\n }\n const llmOk = rhizohLlmEnvConfigured();\n const overallOk = llmOk && firestoreOk;\n const latencyMs = Date.now() - t0;\n sendJson(res, 200, {\n ok: overallOk,\n dns: true,\n llm: llmOk,\n firestore: persistence.mode === \"firebase\" ? firestoreOk : false,\n persistence: persistence.mode,\n firestoreDetail,\n latencyMs,\n service: \"castle-gateway\",\n wsPort: PORT\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/health\") {\n const persistence = getFirebasePersistence();\n sendJson(res, 200, {\n ok: true,\n service: \"castle-gateway\",\n wsPort: PORT,\n persistence: persistence.mode,\n presenceMesh: \"sse\"\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/health\") {\n const workerBase = String(process.env.WORKER_INFRA_URL || \"\").trim();\n const worker = workerBase ? await fetchJsonWithTimeout(`${workerBase.replace(/\\/+$/, \"\")}/infra/health`) : null;\n const scored = scoreHealth(infraMetrics, {\n divergenceTotal: Number(worker?.metrics?.divergenceTotal || 0)\n });\n sendJson(res, 200, {\n ok: true,\n role: \"gateway\",\n queueLagMs: infraMetrics.queueLag,\n queueDepth: infraMetrics.queueDepth,\n errors: infraMetrics.errors,\n status: scored.status,\n score: scored.score,\n reasons: scored.reasons,\n worker: worker\n ? {\n status: worker.status,\n score: worker.score,\n reasons: worker.reasons\n }\n : null,\n timestamp: Date.now()\n });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisRuntime) {\n const payload = await buildGenesisRuntimeSurfacePayloadLive();\n sendJson(res, 200, payload);\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.genesisIngress) {\n if (REQUIRED_GATEWAY_TOKEN) {\n const hdr =\n String(req.headers?.[\"x-castle-gateway-token\"] || req.headers?.[\"authorization\"] || \"\").trim();\n const bearer = hdr.startsWith(\"Bearer \") ? hdr.slice(7).trim() : hdr;\n if (bearer !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const actor = await resolveMeshActor(req);\n const clientId = actor.ok ? actor.clientUid : (await resolveHttpUser(req))?.uid || \"anonymous\";\n const result = ingestGenesisWorldObservationV0(body, { clientId });\n if (!result.ok) {\n const status =\n result.error === \"rate_limited\" || result.error === \"agent_spoke_throttled\"\n ? 429\n : result.error === \"payload_too_large\"\n ? 413\n : 400;\n sendJson(res, status, result);\n return;\n }\n sendJson(res, result.idempotent ? 200 : 202, result);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisStream) {\n const corsOrigin = accessControlAllowOriginValue(req);\n if (!corsOrigin && String(process.env.NODE_ENV || \"\").toLowerCase() === \"production\") {\n const og = String(req.headers?.origin || \"\").trim();\n if (og) {\n console.warn(\"[GATEWAY] genesis SSE: missing Access-Control-Allow-Origin for browser Origin (CORS whitelist mismatch):\", og);\n }\n }\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n res.write(\"retry: 3000\\n\\n\");\n res.write(\": genesis continuity stream\\n\\n\");\n res.write(\"event: genesis\\n\");\n res.write(`data: {\"booted\":true,\"pid\":${process.pid}}\\n\\n`);\n registerGenesisSseClient(res, req);\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLatest) {\n const cp = getLatestGenesisSignedCheckpoint();\n if (!cp) {\n sendJson(res, 404, { ok: false, error: \"no_checkpoint_yet\", hint: \"wait_for_seq_interval_or_configure_signing_secret\" });\n return;\n }\n sendJson(res, 200, { ok: true, checkpoint: cp });\n return;\n }\n\n if (req.method === \"GET\" && pathname.startsWith(rhizohRuntime.routes.genesisCheckpointBySeqPrefix)) {\n const tail = pathname.slice(rhizohRuntime.routes.genesisCheckpointBySeqPrefix.length).split(\"/\")[0];\n const seq = parseInt(String(tail || \"\").trim(), 10);\n const out = await genesisCheckpointQueryBySeqV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"checkpoint_not_found\") {\n sendJson(res, 404, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointRange) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryRangeV0(from, to);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"range_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisCheckpointLineage) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const seq = parseInt(String(u.searchParams.get(\"seq\") || \"\").trim(), 10);\n const out = await genesisCheckpointQueryLineageV0(seq);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"invalid_seq\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"lineage_result_cap_exceeded\") {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisContinuityEvents) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n const to = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const limit = parseInt(String(u.searchParams.get(\"limit\") || \"\").trim(), 10);\n const out = await queryGenesisContinuityEventArchiveV0(from, to, type, limit);\n if (out.error === \"genesis_ephemeral_mode\") {\n sendJson(res, 410, { ok: false, ...out });\n return;\n }\n if (out.error === \"genesis_disk_query_unavailable\" || out.error === \"event_archive_disabled\") {\n sendJson(res, 503, { ok: false, ...out });\n return;\n }\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok && out.error === \"archive_file_too_large\") {\n sendJson(res, 413, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplay) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await resolveGenesisReplayRouterV1({\n from: fromSeq,\n to: toSeq,\n type,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayDiff) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const out = await computeGenesisReplayTemporalDiffV1({ from: fromSeq, to: toSeq, type });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEquivalence) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n const from1 = parseInt(String(u.searchParams.get(\"from1\") || \"\").trim(), 10);\n const to1 = parseInt(String(u.searchParams.get(\"to1\") || \"\").trim(), 10);\n const from2 = parseInt(String(u.searchParams.get(\"from2\") || \"\").trim(), 10);\n const to2 = parseInt(String(u.searchParams.get(\"to2\") || \"\").trim(), 10);\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await compareGenesisReplayEquivalenceV1({\n from1,\n to1,\n from2,\n to2,\n type,\n includeCheckpoints\n });\n if (\n !out.ok &&\n (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")\n ) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayAnalytics) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayAnalyticsV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.genesisReplayEvolution) {\n const u = new URL(String(req.url || \"/\"), `http://${req.headers.host || \"localhost\"}`);\n let fromSeq = parseInt(String(u.searchParams.get(\"from\") || \"\").trim(), 10);\n let toSeq = parseInt(String(u.searchParams.get(\"to\") || \"\").trim(), 10);\n const rangeRaw = String(u.searchParams.get(\"range\") || \"\").trim();\n if ((!Number.isFinite(fromSeq) || !Number.isFinite(toSeq) || fromSeq <= 0 || toSeq <= 0) && rangeRaw) {\n const m = rangeRaw.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (m) {\n fromSeq = parseInt(m[1], 10);\n toSeq = parseInt(m[2], 10);\n }\n }\n const type = String(u.searchParams.get(\"type\") || \"\").trim();\n const bins = parseInt(String(u.searchParams.get(\"bins\") || \"16\").trim(), 10);\n const collapseWindows = parseInt(String(u.searchParams.get(\"collapseWindows\") || \"0\").trim(), 10);\n const checkpointsRaw = String(u.searchParams.get(\"checkpoints\") ?? \"1\").trim().toLowerCase();\n const includeCheckpoints = checkpointsRaw !== \"0\" && checkpointsRaw !== \"false\";\n const out = await computeGenesisReplayEvolutionV1({\n from: fromSeq,\n to: toSeq,\n type,\n bins,\n collapseWindows,\n includeCheckpoints\n });\n if (!out.ok && (out.error === \"invalid_range\" || out.error === \"range_inverted\" || out.error === \"range_span_too_large\")) {\n sendJson(res, 400, { ok: false, ...out });\n return;\n }\n if (!out.ok) {\n sendJson(res, 500, { ok: false, ...out });\n return;\n }\n sendJson(res, 200, { ok: true, ...out });\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/infra/metrics\") {\n res.writeHead(200, { \"Content-Type\": \"text/plain; version=0.0.4; charset=utf-8\" });\n res.end(renderInfraMetricsProm());\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.infraPrometheusMetrics) {\n if (process.env.CASTLE_PROMETHEUS_METRICS !== \"1\") {\n sendJson(res, 404, { ok: false, error: \"prometheus_metrics_disabled\" });\n return;\n }\n res.writeHead(200, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(renderRhizohPrometheusMetrics());\n return;\n }\n if (req.method === \"GET\" && pathname.startsWith(\"/infra/traces/\")) {\n const traceId = decodeURIComponent(pathname.slice(\"/infra/traces/\".length));\n const item = await getTraceById(traceId);\n if (!item) return sendJson(res, 404, { ok: false, error: \"trace_not_found\" });\n return sendJson(res, 200, { ok: true, trace: item });\n }\n if (req.method === \"GET\" && pathname === \"/infra/traces\") {\n const u = new URL(String(req.url || \"/infra/traces\"), `http://${req.headers.host || \"localhost\"}`);\n const sessionId = String(u.searchParams.get(\"sessionId\") || \"\").trim();\n const items = sessionId ? await listTracesBySession(sessionId) : [];\n return sendJson(res, 200, { ok: true, sessionId, traces: items });\n }\n\n /** Presence room mesh (C): JOIN / LEAVE / SNAPSHOT / DELTA / REPLAY (HTTP); SUBSCRIBE = SSE stream. */\n if (req.method === \"POST\" && pathname === \"/presence/mesh/join\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid, clientUid: actor.clientUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/leave\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshLeave(roomUid, actor.clientUid);\n sendJson(res, 200, { ok: true, roomUid });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/snapshot\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n sendJson(res, 200, meshSnapshot(roomUid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/delta\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, MAX_MESSAGE_BYTES);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const seq = meshAppendDelta(roomUid, actor.clientUid, {\n node: body?.node,\n projectionPatch: body?.projectionPatch,\n writerSubject: body?.writerSubject\n });\n sendJson(res, 200, { ok: true, roomUid, seq });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/presence/mesh/replay\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n try {\n const body = await readHttpJson(req, 16 * 1024);\n const roomUid = String(body?.roomUid || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n const fromNodeId = body?.fromNodeId != null ? String(body.fromNodeId) : \"\";\n const fromSeq = body?.fromSeq;\n if (fromNodeId) {\n sendJson(res, 200, meshReplayFromNodeId(roomUid, fromNodeId));\n return;\n }\n sendJson(res, 200, meshReplayFromSeq(roomUid, fromSeq));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === \"/presence/mesh/subscribe\") {\n const actor = await resolveMeshActor(req);\n if (!actor.ok) return sendJson(res, 401, { ok: false, error: actor.reason });\n const q = new URL(String(req.url || \"\"), \"http://localhost\");\n const roomUid = String(q.searchParams.get(\"roomUid\") || \"\").trim();\n if (!roomUid) return sendJson(res, 400, { ok: false, error: \"roomUid_required\" });\n meshJoin(roomUid, actor.clientUid);\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\"\n });\n res.flushHeaders?.();\n const write = (chunk) => {\n try {\n res.write(chunk);\n } catch {\n /* closed */\n }\n };\n const unsub = meshSubscribeSse(roomUid, write);\n const onClose = () => {\n unsub();\n meshLeave(roomUid, actor.clientUid);\n try {\n res.end();\n } catch {\n /* ignore */\n }\n };\n req.on(\"close\", onClose);\n req.on(\"aborted\", onClose);\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, uid: auth.uid, mode: auth.mode, items: await listConnections(auth.uid) });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const id = await createConnection(auth.uid, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"create_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n await updateConnection(auth.uid, id, payload);\n if (payload?.isDefault) await setDefaultConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"update_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"DELETE\" && String(req.url || \"\").startsWith(\"/llm/connections/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n await deleteConnection(auth.uid, id);\n sendJson(res, 200, { ok: true, id, items: await listConnections(auth.uid) });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"delete_connection_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/llm/connections/test\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n if (!checkHttpRateLimit(`llm_conn_test:${auth.uid}`, RL_LLM_CONN_TEST_PER_MIN, 60_000)) {\n return sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n }\n const hasBodyKey = !!String(payload?.apiKey || \"\").trim();\n const result = await queryRhizohLlm(\n {\n message: String(payload?.message || \"Ping from Castle Gateway\"),\n context: payload?.context || { source: \"connection_test\" },\n provider: payload?.provider,\n model: payload?.model,\n apiKey: payload?.apiKey || \"\"\n },\n { allowExternalApiKey: true, llmKeySource: hasBodyKey ? \"auto\" : \"env\" }\n );\n logLlmAccess({\n route: \"/llm/connections/test\",\n uid: auth.uid,\n llmKeyBillingOwner: result.llmKeyBillingOwner,\n llmKeyOrigin: result.llmKeyOrigin,\n provider: result.provider,\n model: result.model,\n connectionId: null\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"connection_test_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAgentIdentities(auth.uid);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/agents/identities\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerAgentIdentity({\n ownerUid: auth.uid,\n agentId: payload?.agentId,\n role: payload?.role,\n personaSeed: payload?.personaSeed || {},\n capabilityLevel: payload?.capabilityLevel\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/agents/identities/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const owns = await getAgentIdentity(id);\n if (!owns || owns.ownerUid !== auth.uid) return sendJson(res, 403, { ok: false, error: \"forbidden\" });\n const payload = await readHttpJson(req);\n const row = await updateAgentIdentity(id, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"agent_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/academy/events\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/academy/events\"}`);\n const status = String(url.searchParams.get(\"status\") || \"\");\n const items = await listAcademyEvents({ ownerUid: auth.uid, status, limit: 120 });\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/academy/events\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const ev = await queueAcademyEvent({\n type: payload?.type,\n roomId: payload?.roomId,\n ownerUid: auth.uid,\n participants: payload?.participants || [],\n topic: payload?.topic || \"academy\",\n payload: payload?.payload || {}\n });\n sendJson(res, 200, { ok: true, event: ev });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && String(req.url || \"\").startsWith(\"/academy/events/\") && String(req.url || \"\").endsWith(\"/resolve\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const parts = String(req.url || \"\").split(\"/\").filter(Boolean);\n const eventId = parts[2] || \"\";\n try {\n const payload = await readHttpJson(req);\n const out = await resolveAcademyEvent(eventId, payload || {});\n sendJson(res, 200, { ok: true, event: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"academy_event_resolve_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/memory\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/memory\"}`);\n const scope = url.searchParams.get(\"scope\") === \"agents\" ? \"agents\" : \"users\";\n const id = String(url.searchParams.get(\"id\") || (scope === \"agents\" ? \"\" : auth.uid));\n const kind = String(url.searchParams.get(\"kind\") || \"\");\n const limit = Number(url.searchParams.get(\"limit\") || 40);\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n sendJson(res, 200, { ok: true, scope, id, kind, items: await listMemories({ scope, id, kind, limit }) });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const profile = await getPersonaGoalMemory(auth.uid);\n sendJson(res, 200, { ok: true, profile });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/profile\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const profile = await setPersonaGoalMemory({\n uid: auth.uid,\n personaPatch: payload?.personaPatch || {},\n goals: Array.isArray(payload?.goals) ? payload.goals : null,\n preferencesPatch: payload?.preferencesPatch || {}\n });\n sendJson(res, 200, { ok: true, profile });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"profile_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/compact\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const out = await autoCompactMemories({ scope, id });\n sendJson(res, 200, { ok: true, scope, id, ...out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_compact_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory/context\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const context = await getMemoryContext({\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n query: String(payload?.query || \"\"),\n limit: Number(payload?.limit || 80)\n });\n sendJson(res, 200, { ok: true, context });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_context_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/place-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const placeName = String(payload?.placeName || \"\").trim();\n if (!placeName) return sendJson(res, 400, { ok: false, error: \"placeName_required\" });\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const persona = String(payload?.waypointPersona || payload?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(placeName)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Place: ${placeName}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Give a concise conversational briefing about this place for a walking-route companion.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-place-brief\", placeName, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `PLACE_BRIEF:${placeName} => ${String(summary.reply || \"\").slice(0, 900)}`,\n tags: [\"place\", \"brief\", \"event-layer\"],\n importance: 0.62,\n meta: { placeName }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"place-brief\",\n text: `${placeName} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: {\n placeName,\n directive: summary.directive,\n lat: hit?.lat ? Number(hit.lat) : null,\n lon: hit?.lon ? Number(hit.lon) : null\n }\n });\n sendJson(res, 200, { ok: true, place: hit || null, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"place_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/route-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const waypoints = Array.isArray(payload?.waypoints) ? payload.waypoints.slice(0, 24) : [];\n const out = [];\n for (const wp of waypoints) {\n const name = String(wp?.name || \"\").trim();\n if (!name) continue;\n const persona = String(wp?.persona || \"guide\");\n const nominatim = `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(name)}`;\n const nomRes = await fetch(nominatim, { headers: { \"User-Agent\": \"castle-gateway/1.0\" } });\n const nomJson = nomRes.ok ? await nomRes.json() : [];\n const hit = Array.isArray(nomJson) ? nomJson[0] : null;\n const raw = [\n `Waypoint: ${name}`,\n hit?.display_name ? `OSM: ${hit.display_name}` : \"\",\n hit?.type ? `Type: ${hit.type}` : \"\",\n hit?.lat && hit?.lon ? `Location: ${hit.lat}, ${hit.lon}` : \"\"\n ]\n .filter(Boolean)\n .join(\"\\n\");\n const summary = await queryRhizohLlm(\n {\n message: `Generate a concise spoken briefing for route waypoint.\\nPersona style: ${persona}\\n${raw}`,\n context: { source: \"event-layer-route-brief\", waypoint: name, persona },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n out.push({\n name,\n persona,\n place: hit || null,\n reply: summary.reply,\n directive: summary.directive\n });\n }\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `ROUTE_BRIEF: waypoints=${out.length} ${out.map((x) => `${x.name}:${x.persona}`).join(\" | \")}`.slice(0, 1300),\n tags: [\"route\", \"waypoint\", \"brief\", \"event-layer\"],\n importance: 0.74,\n meta: { count: out.length }\n });\n await addTranscript(auth.uid, {\n source: \"route-companion\",\n eventType: \"route-brief\",\n text: out.map((x) => `${x.name}:${String(x.reply || \"\").slice(0, 140)}`).join(\" | \").slice(0, 1400),\n roomId: \"studio-main\",\n meta: {\n waypointCount: out.length,\n waypoints: out.map((x) => ({\n name: x.name,\n persona: x.persona,\n lat: x.place?.lat ? Number(x.place.lat) : null,\n lon: x.place?.lon ? Number(x.place.lon) : null\n }))\n }\n });\n sendJson(res, 200, { ok: true, items: out });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"route_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/event-layer/pdf-brief\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const title = String(payload?.title || \"document\");\n const conn = await resolveConnection(auth.uid, String(payload?.connectionId || \"\"));\n const providedText = String(payload?.text || \"\");\n const url = String(payload?.url || \"\");\n let content = providedText;\n if (!content && url) content = await fetchTextFromUrl(url);\n if (!content) return sendJson(res, 400, { ok: false, error: \"text_or_supported_url_required\" });\n const summary = await queryRhizohLlm(\n {\n message: `Summarize this document and provide key points + practical actions.\\nTitle:${title}\\nContent:\\n${content.slice(0, 12000)}`,\n context: { source: \"event-layer-pdf-brief\", title },\n provider: conn?.provider,\n model: conn?.model,\n apiKey: conn?.apiKey || \"\"\n },\n { llmKeySource: conn?.apiKey ? \"user_connection\" : \"env\" }\n );\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n kind: \"semantic\",\n text: `DOC_BRIEF:${title} => ${String(summary.reply || \"\").slice(0, 1000)}`,\n tags: [\"document\", \"brief\", \"library\", \"event-layer\"],\n importance: 0.7,\n meta: { title, url: url || null }\n });\n await addTranscript(auth.uid, {\n source: \"library-brief\",\n eventType: \"pdf-brief\",\n text: `${title} -> ${String(summary.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { title }\n });\n sendJson(res, 200, { ok: true, reply: summary.reply, directive: summary.directive });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_brief_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/pdf/upload\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req, 8 * 1024 * 1024);\n const job = await enqueuePdfJob({\n uid: auth.uid,\n fileName: payload?.fileName || \"document.pdf\",\n bytesBase64: payload?.contentBase64\n });\n sendJson(res, 200, { ok: true, job });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"pdf_upload_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/session\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createPublishSession(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session\",\n text: `Publish session armed: ${row.protocol} ${row.target}`.slice(0, 500),\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: row.id, bridge: row.bridge }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/studio/publish/sessions\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listPublishSessions(auth.uid, 80);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/publish/health-check\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const protocol = String(payload?.protocol || \"WHIP\");\n const target = String(payload?.target || \"\");\n const health = await checkPublishTargetHealth(protocol, target);\n if (payload?.sessionId) {\n await updatePublishSession(auth.uid, String(payload.sessionId), {\n status: health.ok ? \"healthy\" : \"degraded\",\n reconnectPolicy: {\n enabled: true,\n backoffSec: [2, 5, 10, 20],\n nextRetryInSec: health.reconnectInSec || 0,\n maxRetries: 8,\n reason: health.reason\n }\n });\n }\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-health\",\n text: `${protocol} ${target} => ${health.ok ? \"healthy\" : \"degraded\"} (${health.reason})`,\n roomId: \"studio-main\",\n meta: { protocol, target, reconnect: !!health.shouldReconnect }\n });\n sendJson(res, 200, { ok: true, health });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_health_check_failed\" });\n }\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/studio/publish/sessions/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updatePublishSession(auth.uid, id, payload || {});\n await addTranscript(auth.uid, {\n source: \"studio\",\n eventType: \"publish-session-update\",\n text: `Session ${id} -> ${row.status || \"updated\"}`,\n roomId: row.roomId || \"studio-main\",\n meta: { sessionId: id, status: row.status, ingestUrl: row.ingestUrl || \"\" }\n });\n sendJson(res, 200, { ok: true, session: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"publish_session_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/transcripts\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/transcripts\"}`);\n const limit = Math.max(1, Math.min(200, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await listTranscripts(auth.uid, limit);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/studio/transcripts\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await addTranscript(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"transcript_write_failed\" });\n }\n return;\n }\n\n /** E1: broadcast intent → GreenRoom capability → transcript + memory artifact */\n if (req.method === \"POST\" && req.url === \"/studio/capabilities/greenroom\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const traceId = String(payload?.traceId || `TRC-${Date.now().toString(36).toUpperCase()}`);\n const title = String(payload?.title || \"Castle Live Broadcast\").slice(0, 200);\n const intentRaw = String(payload?.intentRaw || \"\").slice(0, 800);\n const audienceEstimate = Math.max(0, Math.min(50_000_000, Number(payload?.audienceEstimate) || 1200));\n const roomId = String(payload?.roomId || \"greenroom-main\");\n const transcriptRow = await addTranscript(auth.uid, {\n source: \"rhizoh\",\n eventType: \"BROADCAST_ROUTED\",\n text: `GreenRoom · ${title}${intentRaw ? `\\nIntent: ${intentRaw}` : \"\"}`,\n roomId,\n meta: {\n ack: \"BROADCAST_ROUTED\",\n capability: \"greenroom\",\n intentType: \"broadcast\",\n traceId,\n audienceEstimate,\n artifact: {\n kind: \"broadcast_card\",\n title,\n traceId,\n roomId,\n createdAt: Date.now()\n }\n }\n });\n await appendMemory({\n scope: \"users\",\n id: auth.uid,\n text: `GREENROOM_BROADCAST:${title} trace=${traceId}`,\n tags: [\"broadcast\", \"greenroom\", \"artifact\", \"castle-library\"],\n importance: 0.72,\n kind: \"episodic\",\n meta: { traceId, capability: \"greenroom\", transcriptId: transcriptRow.id }\n });\n sendJson(res, 200, {\n ok: true,\n ack: \"BROADCAST_ROUTED\",\n traceId,\n transcript: transcriptRow,\n replayPath: `/replay/${encodeURIComponent(traceId)}`,\n sharePath: `/replay/${encodeURIComponent(traceId)}`\n });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"greenroom_capability_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/studio/metadata/search\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const url = new URL(`http://localhost${req.url || \"/studio/metadata/search\"}`);\n const q = String(url.searchParams.get(\"q\") || \"\");\n const limit = Math.max(1, Math.min(300, Number(url.searchParams.get(\"limit\") || 80)));\n const items = await searchMetadata(auth.uid, q, limit);\n sendJson(res, 200, { ok: true, q, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerDevice(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, device: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"device_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const lat = Number(payload?.lat);\n const lon = Number(payload?.lon);\n const speed = Math.max(0, Number(payload?.speed || 0));\n const cfgCenterLat = Number(process.env.CASTLE_GEOFENCE_CENTER_LAT || 41.0082);\n const cfgCenterLon = Number(process.env.CASTLE_GEOFENCE_CENTER_LON || 28.9784);\n const cfgRadiusM = Math.max(100, Number(process.env.CASTLE_GEOFENCE_RADIUS_M || 55000));\n const cfgSpeedCap = Math.max(1, Number(process.env.CASTLE_SPEED_CAP_MPS || 35));\n const dist = Number.isFinite(lat) && Number.isFinite(lon) ? haversineMeters(cfgCenterLat, cfgCenterLon, lat, lon) : 0;\n const geofenceOk = !Number.isFinite(dist) ? true : dist <= cfgRadiusM;\n const speedOk = speed <= cfgSpeedCap;\n const failsafe = !(geofenceOk && speedOk);\n const safety = {\n geofenceOk,\n speedOk,\n failsafe,\n distanceFromCenterM: Number.isFinite(dist) ? dist : null,\n speedCapMps: cfgSpeedCap\n };\n const row = await appendTelemetry(auth.uid, deviceId, {\n lat,\n lon,\n speed,\n battery: payload?.battery,\n mode: payload?.mode || (failsafe ? \"FAILSAFE_HOLD\" : \"ACTIVE\"),\n raw: payload?.raw || {}\n });\n await updateDevice(auth.uid, deviceId, {\n status: failsafe ? \"failsafe\" : \"active\",\n lastTelemetryAt: row.ts,\n safetyState: safety\n });\n if (failsafe) {\n await addTranscript(auth.uid, {\n source: \"robotics-guardrails\",\n eventType: \"failsafe\",\n text: `Device ${deviceId} failsafe triggered geofenceOk=${geofenceOk} speedOk=${speedOk}`,\n roomId: \"studio-main\",\n meta: { deviceId, safety }\n });\n }\n sendJson(res, 200, { ok: true, telemetry: row, safety, command: failsafe ? \"FAILSAFE_HOLD\" : \"CONTINUE\" });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"telemetry_ingest_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/telemetry\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listTelemetry(auth.uid, 150);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await createAutomation(auth.uid, payload || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"automation-scheduled\",\n text: `Automation scheduled: ${row.title} at ${new Date(row.scheduleAt).toLocaleString()}`,\n roomId: \"studio-main\",\n meta: { automationId: row.id, action: row.action, deviceId: row.deviceId }\n });\n sendJson(res, 200, { ok: true, automation: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"automation_create_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/automations\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listAutomations(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/devices\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listDevices(auth.uid, 120);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const deviceId = String(payload?.deviceId || \"\");\n if (!deviceId) return sendJson(res, 400, { ok: false, error: \"deviceId_required\" });\n const devs = await listDevices(auth.uid, 500);\n const d = devs.find((x) => x.id === deviceId);\n if (!d) return sendJson(res, 404, { ok: false, error: \"device_not_found\" });\n const action = String(payload?.action || \"\");\n const caps = Array.isArray(d.capabilityProfile) ? d.capabilityProfile : [];\n if (action && caps.length && !caps.includes(action) && !caps.includes(\"*\")) {\n return sendJson(res, 400, { ok: false, error: \"unsupported_action_for_device\", capabilityProfile: caps });\n }\n const cmd = await enqueueCommand(auth.uid, {\n deviceId,\n action: action || \"NOOP\",\n params: payload?.params || {},\n adapter: payload?.adapter || d.adapter || \"websocket\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-command-queue\",\n eventType: \"command-queued\",\n text: `${cmd.deviceId} -> ${cmd.action}`,\n roomId: \"studio-main\",\n meta: { commandId: cmd.id, adapter: cmd.adapter }\n });\n sendJson(res, 200, { ok: true, command: cmd });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_queue_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/robotics/commands\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listCommands(auth.uid, 200);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"PATCH\" && String(req.url || \"\").startsWith(\"/robotics/commands/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n try {\n const payload = await readHttpJson(req);\n const row = await updateCommandStatus(auth.uid, id, payload?.status || \"sent\");\n sendJson(res, 200, { ok: true, command: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"command_update_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/rhizoh/bridge\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const result = await runRhizohBrainV2({\n message: String(payload?.message || \"robotics bridge command\"),\n uid: auth.uid,\n agentId: String(payload?.agentId || \"\"),\n worldState: payload?.worldState || {},\n deviceState: payload?.deviceState || {},\n intent: \"robotics-mechanics\"\n });\n await addTranscript(auth.uid, {\n source: \"robotics-bridge\",\n eventType: \"robotics-command\",\n text: `Bridge -> ${String(result.reply || \"\").slice(0, 500)}`,\n roomId: \"studio-main\",\n meta: { directive: result.directive, agentId: payload?.agentId || \"\" }\n });\n sendJson(res, 200, { ok: true, result });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"robotics_bridge_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/planner/graph\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const graph = buildTaskActionGraph(String(payload?.task || \"\"), payload?.context || {});\n await addTranscript(auth.uid, {\n source: \"planner\",\n eventType: \"planner-graph\",\n text: `Planner graph built: ${graph.taskTitle} nodes=${graph.nodes.length}`,\n roomId: \"studio-main\",\n meta: { taskTitle: graph.taskTitle, nodeCount: graph.nodes.length }\n });\n sendJson(res, 200, { ok: true, graph });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"planner_graph_failed\" });\n }\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const row = await registerSocialChannel(auth.uid, payload || {});\n sendJson(res, 200, { ok: true, channel: row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_channel_register_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/channels\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const items = await listSocialChannels(auth.uid, 100);\n sendJson(res, 200, { ok: true, items });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/social/broadcast\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const text = String(payload?.text || \"\").slice(0, 1200);\n const channels = await listSocialChannels(auth.uid, 100);\n const enabled = channels.filter((c) => c.status === \"enabled\");\n const deliveries = [];\n for (const c of enabled) {\n const out = await deliverSocialWithRetry(c, text);\n deliveries.push({ id: c.id, platform: c.platform, ...out });\n }\n const okCount = deliveries.filter((d) => d.ok).length;\n await addTranscript(auth.uid, {\n source: \"social-layer\",\n eventType: \"social-broadcast\",\n text: `Broadcast delivered ${okCount}/${enabled.length} channel(s): ${text.slice(0, 240)}`,\n roomId: \"studio-main\",\n meta: { deliveries }\n });\n sendJson(res, 200, { ok: true, sentChannels: okCount, totalChannels: enabled.length, deliveries });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"social_broadcast_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && req.url === \"/social/ethics/programs\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n sendJson(res, 200, { ok: true, ...buildEthicsPrograms() });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/robotics/commands/dispatch\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const all = await listCommands(auth.uid, 200);\n const queued = all.filter((c) => String(c.status || \"queued\") === \"queued\").slice(0, 50);\n const devices = await listDevices(auth.uid, 500);\n const byId = new Map(devices.map((d) => [d.id, d]));\n const results = [];\n for (const cmd of queued) {\n const dev = byId.get(cmd.deviceId);\n if (!dev) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: \"device_not_found\" });\n continue;\n }\n try {\n const dispatched = await dispatchCommandToAdapter(dev, cmd);\n await updateCommandStatus(auth.uid, cmd.id, \"sent\");\n results.push({ id: cmd.id, ok: true, adapter: dispatched.adapter, via: dispatched.via });\n } catch (error) {\n await updateCommandStatus(auth.uid, cmd.id, \"failed\");\n results.push({ id: cmd.id, ok: false, error: String(error?.message || \"dispatch_failed\") });\n }\n }\n await addTranscript(auth.uid, {\n source: \"robotics-command-dispatch\",\n eventType: \"command-dispatch-batch\",\n text: `Dispatch batch size=${results.length} ok=${results.filter((x) => x.ok).length}`,\n roomId: \"studio-main\",\n meta: { results: results.slice(0, 30) }\n });\n sendJson(res, 200, { ok: true, processed: results.length, results });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"dispatch_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && String(req.url || \"\").startsWith(\"/pdf/jobs/\")) {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n const id = String(req.url || \"\").split(\"/\").pop() || \"\";\n const job = await getPdfJob(auth.uid, id);\n if (!job) return sendJson(res, 404, { ok: false, error: \"pdf_job_not_found\" });\n sendJson(res, 200, { ok: true, job });\n return;\n }\n\n if (req.method === \"POST\" && req.url === \"/memory\") {\n const auth = await resolveHttpUser(req);\n if (!auth.ok) return sendJson(res, 401, { ok: false, error: auth.reason });\n try {\n const payload = await readHttpJson(req);\n const scope = payload?.scope === \"agents\" ? \"agents\" : \"users\";\n const id = String(payload?.id || (scope === \"users\" ? auth.uid : \"\"));\n if (!id) return sendJson(res, 400, { ok: false, error: \"memory_id_required\" });\n const row = await appendMemory({\n scope,\n id,\n text: payload?.text,\n tags: payload?.tags || [],\n importance: payload?.importance,\n kind: payload?.kind || \"episodic\",\n meta: { uid: auth.uid, ...(payload?.meta || {}) }\n });\n sendJson(res, 200, { ok: true, row });\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error?.message || \"memory_write_failed\" });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.externalTruth) {\n try {\n if (process.env.RHIZOH_EXTERNAL_TRUTH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_truth:${ip}`, RL_RHIZOH_EXTERNAL_TRUTH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n sendJson(res, 200, buildRhizohExternalGroundTruthPayload(req));\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.productOutcome) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const rawBody = await readHttpBodyText(req, 64 * 1024);\n const body = rawBody.trim() ? safeJsonParse(rawBody) : null;\n if (!body || typeof body !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"invalid_json_payload\" });\n return;\n }\n const sigHdr = String(req.headers?.[\"x-rhizoh-outcome-signature\"] || \"\").trim();\n const hmacV = verifyRhizohOutcomeHmac(rawBody, sigHdr);\n if (!hmacV.ok) {\n sendJson(res, 401, { ok: false, error: hmacV.error || \"outcome_hmac_invalid\" });\n return;\n }\n const srcTokHdr = String(req.headers?.[\"x-rhizoh-outcome-source-token\"] || \"\").trim();\n const srcV = verifyRhizohOutcomeSourceToken(body.source, srcTokHdr);\n if (!srcV.ok) {\n sendJson(res, 401, { ok: false, error: srcV.error || \"outcome_source_token_invalid\" });\n return;\n }\n const subjectKey = String(body.subjectKey || \"\").trim().slice(0, 128);\n if (\n subjectKey &&\n !checkHttpRateLimit(\n `rhizoh_product_outcome_subj:${subjectKey}`,\n RL_RHIZOH_PRODUCT_OUTCOME_SUBJECT_PER_MIN,\n 60_000\n )\n ) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_subject\" });\n return;\n }\n const result = ingestRhizohProductOutcomeHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n schemaVersion: \"1.1.0\",\n aggregate: result.aggregate,\n integrity: result.integrity\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"outcome_failed\") });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.productOutcomeAggregate) {\n try {\n if (process.env.RHIZOH_PRODUCT_OUTCOME_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_product_outcome_agg:${ip}`, RL_RHIZOH_PRODUCT_OUTCOME_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const u = new URL(String(req.url || \"/\"), \"http://localhost\");\n const cohortId = u.searchParams.get(\"cohortId\") || \"\";\n const decisionFingerprint = u.searchParams.get(\"decisionFingerprint\") || \"\";\n const limit = u.searchParams.get(\"limit\") || \"50\";\n const rows = listRhizohProductOutcomeAggregates({\n cohortId: cohortId || undefined,\n decisionFingerprint: decisionFingerprint || undefined,\n limit: Number(limit) || 50\n });\n sendJson(res, 200, { ok: true, schemaVersion: \"1.1.0\", rows });\n } catch (e) {\n sendJson(res, 500, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.externalLossBatch) {\n try {\n if (process.env.RHIZOH_EXTERNAL_LOSS_BATCH_REQUIRE_TOKEN === \"1\" && REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`rhizoh_external_loss_batch:${ip}`, RL_RHIZOH_EXTERNAL_LOSS_BATCH_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const result = ingestRhizohExternalLossBatchHttp(body, { ip });\n if (!result.ok) {\n sendJson(res, 400, { ok: false, error: result.error || \"ingest_failed\" });\n return;\n }\n sendJson(res, 200, { ok: true, accepted: result.accepted ?? 0 });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"batch_failed\") });\n }\n return;\n }\n\n function moderationAdminOk(req) {\n const expected = String(process.env.CASTLE_MODERATION_ADMIN_KEY || \"\").trim();\n if (!expected) return false;\n const got = String(req.headers[\"x-castle-moderation-key\"] || \"\").trim();\n return got.length > 0 && got === expected;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.opsAbuseReport) {\n try {\n const body = await readHttpJson(req, 32 * 1024);\n const auth = await resolveHttpUser(req);\n const entry = submitAbuseReportV0({\n reporterUid: auth.ok ? auth.uid : null,\n targetUid: body?.targetUid ?? null,\n category: body?.category,\n detail: body?.detail,\n traceId: body?.traceId ?? null\n });\n sendJson(res, 202, { ok: true, reportId: entry.id, status: entry.status });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsModerationQueue) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n sendJson(res, 200, {\n ok: true,\n queue: listModerationQueueV0(100),\n softBlocked: listSoftBlockedUsersV0()\n });\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-block\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n const out = softBlockUserV0(body?.uid, body?.reason);\n sendJson(res, out.ok ? 200 : 400, out);\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === \"/rhizoh/ops/moderation/soft-unblock\") {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n try {\n const body = await readHttpJson(req, 8 * 1024);\n sendJson(res, 200, removeSoftBlockV0(body?.uid));\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e) });\n }\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsAgentSnapshots) {\n if (!moderationAdminOk(req)) {\n sendJson(res, 403, { ok: false, error: \"moderation_admin_required\" });\n return;\n }\n const limit = Number(new URL(req.url || \"\", \"http://local\").searchParams.get(\"limit\") || 50);\n sendJson(res, 200, { ok: true, snapshots: listRecentAgentSnapshotsV0(limit) });\n return;\n }\n\n if (req.method === \"GET\" && pathname === rhizohRuntime.routes.opsHardeningStatus) {\n const auth = await resolveHttpUser(req);\n const principal = auth.ok ? `uid:${auth.uid}` : null;\n const gclSnap = await getGlobalCostLedgerSnapshotV0(principal);\n const economicStrategy = evaluateEconomicStrategyL3V0(gclSnap);\n const hardeningTenantId = resolveHardeningTenantIdV0(auth, req);\n const unifiedState = await buildUnifiedStateNarrativeV0({\n tenantId: hardeningTenantId ?? undefined,\n platformScope: !hardeningTenantId,\n principal\n });\n sendJson(res, 200, {\n ok: true,\n schema: \"rhizoh.ops.hardening_status.v0\",\n containment: readAgentContainmentConfigV0(),\n cost: readCostContainmentConfigV0(),\n gcl: {\n backend: await getGclBackendStatusV0(),\n snapshot: gclSnap\n },\n economicStrategyL3: economicStrategy,\n phasedRollout: await getPhasedRolloutStatsV0(),\n tier: readPhasedRolloutTierV0(),\n principalCost: auth.ok ? await getCostContainmentStatsV0(principal) : null,\n stressTaxonomy: verifyStressTaxonomyCoverageV0(),\n stressConflictResolution: verifyStressConflictResolutionV0(),\n resolutionStabilityEnvelope: verifyResolutionStabilityEnvelopeV0(),\n unifiedState: {\n tenantScope: {\n tenantId: unifiedState.tenantScope?.tenantId,\n scope: unifiedState.tenantScope?.scope,\n noGlobalDerivedState: unifiedState.tenantScope?.noGlobalDerivedState\n },\n narrativeFingerprint: unifiedState.narrativeFingerprint,\n screenshotScopeWatermark: unifiedState.screenshotScopeWatermark,\n interpretationSafetyContract: unifiedState.interpretationSafetyContract,\n interpretationUxContract: {\n schema: unifiedState.interpretationUxContract?.schema,\n layerOrder: unifiedState.interpretationUxContract?.layerOrder,\n narrativeNeverVisuallyDominant: unifiedState.interpretationUxContract?.narrativeNeverVisuallyDominant\n },\n governance: {\n narrativeIsNotDecisionLayer: unifiedState.governance.narrativeIsNotDecisionLayer,\n displayOrder: unifiedState.governance.displayOrder,\n trustPosture: unifiedState.governance.trustPosture,\n interpretationUxContract: unifiedState.governance.interpretationUxContract?.schema\n },\n humanOps: {\n decisionTier: unifiedState.humanOps?.humanDecisionScaling?.tier,\n decisionOwner: unifiedState.humanOps?.humanDecisionScaling?.decisionOwner,\n opsRouteId: unifiedState.humanOps?.humanDecisionOpsRunbook?.routing?.routeId,\n slaAckMinutes: unifiedState.humanOps?.humanDecisionOpsRunbook?.sla?.ackMinutes,\n uiDisclaimerRequired: unifiedState.humanOps?.narrativeUiSafety?.displayRules?.requireDisclaimerBanner,\n misreadHighResidual: unifiedState.humanOps?.misreadSimulation?.highResidualCount,\n propagationHighResidual: unifiedState.humanOps?.socialPropagationSimulation?.highResidualCount,\n worstWatermarkSurvivability:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.worstWatermarkSurvivability,\n dominantDistortionSource:\n unifiedState.humanOps?.socialPropagationSimulation?.aggregate?.dominantDistortionSource\n },\n culturalRisk: {\n perceptualRiskClass: unifiedState.culturalRisk?.perceptualRiskClass,\n trustFork: unifiedState.culturalRisk?.trustDynamics?.fork,\n trustDecayScore: unifiedState.culturalRisk?.trustDynamics?.scores?.trustDecay,\n mythologyScore: unifiedState.culturalRisk?.trustDynamics?.scores?.mythology,\n confidenceDetachmentExposure:\n unifiedState.culturalRisk?.trustDynamics?.axes?.confidenceDetachmentExposure\n },\n appliedSystemsLayer: {\n schema: unifiedState.appliedSystemsLayer?.schema,\n researchOutputType: unifiedState.appliedSystemsLayer?.researchGovernance?.outputType,\n roboticsBlockActuation: unifiedState.appliedSystemsLayer?.roboticsGrounding?.blockActuation,\n spiralRumorCount:\n unifiedState.appliedSystemsLayer?.spiralMMOGameKernel?.gameLayer?.rumors?.length\n },\n actionSemanticGovernance: {\n schema: unifiedState.actionSemanticGovernance?.schema,\n highRiskActionCount: unifiedState.actionSemanticGovernance?.highRiskActionCount,\n loopClosed: unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.loopClosed,\n authorityCollapseMitigated:\n unifiedState.actionSemanticGovernance?.epistemicFeedbackLoop?.risks?.authorityCollapse\n ?.mitigated\n },\n actionContextResolution: {\n schema: unifiedState.actionContextResolution?.schema,\n contextFingerprint: unifiedState.actionContextResolution?.contextFingerprint?.fingerprintHash,\n domainCollision: unifiedState.actionContextResolution?.domainCollision?.detected,\n deployAgentSpiralBlocked:\n unifiedState.actionContextResolution?.actionOverloadingRisk?.deployAgentExample\n ?.spiral_mmo === \"blocked\",\n roboticsActuationEligible:\n unifiedState.actionContextResolution?.roboticsActuationCorrectness?.actuationEligible\n },\n epistemicCoherence: {\n schema: unifiedState.epistemicCoherence?.schema,\n verdict: unifiedState.epistemicCoherence?.systemCoherence?.verdict,\n coherenceScore: unifiedState.epistemicCoherence?.systemCoherence?.score,\n contradictionCount: unifiedState.epistemicCoherence?.crossLayerContradictions?.count,\n operatorDecision: unifiedState.epistemicCoherence?.uxCompression?.operatorDecision,\n headlineTr: unifiedState.epistemicCoherence?.uxCompression?.headline?.tr,\n falseSafetyWarning: Boolean(\n unifiedState.epistemicCoherence?.uxCompression?.falseSafetyWarning\n )\n },\n coherenceAuthorityBoundary: {\n schema: unifiedState.coherenceAuthorityBoundary?.schema,\n bindingValid: unifiedState.coherenceAuthorityBoundary?.eclAuthorityBinding?.valid,\n canExecute: unifiedState.coherenceAuthorityBoundary?.contract?.can_execute,\n mandatoryBannerTr:\n unifiedState.coherenceAuthorityBoundary?.disclaimers?.mandatoryBanner?.tr\n },\n decisionLatencyGovernance: {\n schema: unifiedState.decisionLatencyGovernance?.schema,\n latencyTier: unifiedState.decisionLatencyGovernance?.latencyTier,\n inflationScore: unifiedState.decisionLatencyGovernance?.latencyInflation?.score,\n fastPathEligible: unifiedState.decisionLatencyGovernance?.fastPath?.eligible,\n primaryActionId: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.primaryActionId,\n ackSlaMinutes: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.ackSlaMinutes,\n certaintyCap: unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.certaintyCap,\n uncertaintyItems:\n unifiedState.decisionLatencyGovernance?.humanDecisionPacket?.uncertaintyEnvelope?.itemCount\n },\n decisionPacketUncertaintyBoundary: {\n schema: unifiedState.decisionPacketUncertaintyBoundary?.schema,\n invariantValid: unifiedState.decisionPacketUncertaintyBoundary?.invariantCheck?.valid,\n centralInvariant: unifiedState.decisionPacketUncertaintyBoundary?.centralInvariant\n },\n epistemicDecisionPacing: {\n schema: unifiedState.epistemicDecisionPacing?.schema,\n queueSaturationTier:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation?.tier,\n operatorProcessingLatency:\n unifiedState.epistemicDecisionPacing?.operatorPacingControl?.queueSaturation\n ?.operatorProcessingLatency,\n postureWindowCount:\n unifiedState.epistemicDecisionPacing?.temporaryStaticPostureWindows\n ?.temporaryStaticPostureWindows?.windowCount,\n executionBoundariesValid:\n unifiedState.epistemicDecisionPacing?.executionBoundaries?.valid,\n grantsExecutionApproval: unifiedState.epistemicDecisionPacing?.grantsExecutionApproval\n },\n epistemicTemporalCoherence: {\n schema: unifiedState.epistemicTemporalCoherence?.schema,\n allInvariantsValid: unifiedState.epistemicTemporalCoherence?.allInvariantsValid,\n temporalAlignmentValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.temporalAlignment?.valid,\n crossWindowGuardValid:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard?.valid,\n delayedTruthValid: unifiedState.epistemicTemporalCoherence?.invariants?.delayedTruthRule?.valid,\n reconciliationRequired:\n unifiedState.epistemicTemporalCoherence?.invariants?.crossWindowContradictionGuard\n ?.reconciliation?.required\n },\n realityDriftObserver: {\n schema: unifiedState.realityDriftObserver?.schema,\n driftActive: unifiedState.realityDriftObserver?.driftObserverSummary?.active,\n misapprehensionShapeCount:\n unifiedState.realityDriftObserver?.misapprehensionShapeCatalog?.shapeCount,\n criticalShapeCount:\n unifiedState.realityDriftObserver?.driftObserverSummary?.criticalShapeCount,\n roboticsMismatchActive:\n unifiedState.realityDriftObserver?.roboticsFeedbackMismatch?.active\n },\n driftCausality: {\n schema: unifiedState.driftCausality?.schema,\n shapesWithCausalExplanation: unifiedState.driftCausality?.rdolLinkage?.shapesWithCausalExplanation,\n activeDomainCount: unifiedState.driftCausality?.causalitySummary?.activeDomainCount,\n propagationCausalityActive: unifiedState.driftCausality?.causalDomains?.propagation?.active\n },\n productReadiness: unifiedState.productReadiness,\n stateLayers: {\n raw: unifiedState.stateLayers.raw,\n derived: {\n systemState: unifiedState.stateLayers.derived.systemState,\n narrative: unifiedState.stateLayers.derived.narrative,\n validation: unifiedState.stateLayers.derived.validation\n },\n policy: {\n binding: unifiedState.stateLayers.policy.binding,\n decisionOwner: unifiedState.stateLayers.policy.decisionOwner,\n prohibitedActions: unifiedState.stateLayers.policy.prohibitedActions\n }\n },\n systemState: unifiedState.systemState,\n interpretation: unifiedState.interpretation,\n validation: {\n trustPosture: unifiedState.validation.trustPosture,\n adjustedConfidence: unifiedState.validation.adjustedConfidence,\n agreement: unifiedState.validation.confidenceDecomposition.agreement,\n divergenceScore: unifiedState.validation.divergence.divergenceScore,\n validated: unifiedState.validation.divergence.validated,\n uncertaintyTags: unifiedState.validation.uncertainty.tags.map((t) => t.id),\n flags: unifiedState.validation.divergence.flags.map((f) => f.id)\n },\n compression: unifiedState.compression\n }\n });\n return;\n }\n\n if (req.method === \"POST\" && req.url === rhizohRuntime.routes.rhizohLlm) {\n try {\n const payload = await readHttpJson(req);\n const auth = await resolveHttpUser(req);\n const ip = getHttpClientIp(req);\n logRhizohHealth(\"gateway_accept\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId: String(payload?.traceId || \"\"),\n auth: auth.ok ? \"ok\" : \"anon\",\n ip\n });\n const rlKey = auth.ok ? `uid:${auth.uid}` : `ip:${ip}`;\n if (!checkHttpRateLimit(`rhizoh_llm:${rlKey}`, RL_RHIZOH_LLM_PER_MIN, 60_000)) {\n const rlTaxonomy = classifyStressResponseV0({ code: \"rate_limit_exceeded\" });\n return sendJson(res, 429, {\n ok: false,\n error: \"rate_limit_exceeded\",\n reply: rlTaxonomy.userMessageTr || \"İstek sınırı aşıldı. Kısa süre sonra tekrar deneyin.\",\n stressClass: rlTaxonomy.stressClass,\n responseAction: rlTaxonomy.responseAction,\n stressMatrix: rlTaxonomy.matrix,\n stressInterpretable: rlTaxonomy.interpretable,\n stressConfidence: rlTaxonomy.stressConfidence,\n actionConfidence: rlTaxonomy.actionConfidence,\n actionSoftened: rlTaxonomy.actionSoftened,\n responseActionStrict: rlTaxonomy.responseActionStrict,\n actionInterpretable: rlTaxonomy.actionInterpretable,\n conflictResolution: rlTaxonomy.conflictResolution,\n directive: \"NONE\"\n });\n }\n\n const requireExplicit = process.env.CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE === \"1\";\n const keyMode = normalizeClientLlmKeySource(payload?.llmKeySource ?? payload?.keySource);\n if (keyMode === null) {\n return sendJson(res, 400, {\n ok: false,\n error: \"invalid_llm_key_source\",\n reply: \"llmKeySource geçersiz. Kullanın: env | user_connection | auto\",\n directive: \"NONE\"\n });\n }\n if (requireExplicit && keyMode === \"auto\") {\n return sendJson(res, 400, {\n ok: false,\n error: \"llm_key_source_required\",\n reply: \"Sunucu CASTLE_LLM_REQUIRE_EXPLICIT_KEY_SOURCE=1: istekte llmKeySource (env veya user_connection) zorunlu.\",\n directive: \"NONE\"\n });\n }\n\n const conn = auth.ok ? await resolveConnection(auth.uid, String(payload?.connectionId || \"\")) : null;\n\n if (keyMode === \"user_connection\") {\n if (!auth.ok) {\n return sendJson(res, 401, {\n ok: false,\n error: \"llm_auth_required_for_user_connection\",\n reply: \"Kayıtlı LLM bağlantısı için giriş gerekli.\",\n directive: \"NONE\"\n });\n }\n if (!conn?.apiKey) {\n return sendJson(res, 400, {\n ok: false,\n error: \"user_llm_connection_required\",\n reply: \"Geçerli bir LLM bağlantısı yok; Studio’da bağlantı ekleyin veya llmKeySource=env kullanın.\",\n directive: \"NONE\"\n });\n }\n }\n\n const safePayload = { ...(payload || {}) };\n delete safePayload.apiKey;\n delete safePayload.llmKeySource;\n delete safePayload.keySource;\n\n let resolvedProvider = payload?.provider;\n let resolvedModel = payload?.model;\n let connApiKey = \"\";\n\n if (keyMode === \"user_connection\") {\n resolvedProvider = conn.provider;\n resolvedModel = conn.model;\n connApiKey = conn.apiKey || \"\";\n } else if (keyMode === \"env\") {\n connApiKey = \"\";\n } else {\n resolvedProvider = conn?.provider || payload?.provider;\n resolvedModel = conn?.model || payload?.model;\n connApiKey = conn?.apiKey || \"\";\n }\n\n const { result, traceId, turnLatencyMs, spinePhases, sampledTrace } = await rhizohGatewayTurn({\n safePayload,\n auth,\n keyMode,\n conn,\n resolvedProvider,\n resolvedModel,\n connApiKey\n });\n logRhizohHealth(\"llm_response\", {\n route: rhizohRuntime.routes.rhizohLlm,\n traceId,\n turnLatencyMs: Number(turnLatencyMs || 0),\n keyMode\n });\n sendJson(res, 200, {\n ...result,\n connectionId: conn?.id || null,\n llmKeySourceUsed: keyMode,\n traceId,\n turnLatencyMs,\n ...(spinePhases ? { spinePhases } : {}),\n ...(sampledTrace !== undefined ? { sampledTrace } : {})\n });\n } catch (error) {\n const msg = String(error?.message || \"\");\n const code = error?.code || \"\";\n let status = 500;\n if (msg === \"rate_limit_exceeded\") status = 429;\n else if (\n code === \"cost_hard_limit\" ||\n code === \"phased_rollout_capacity\" ||\n error?.containment === true ||\n String(code).startsWith(\"agent_\")\n ) {\n status = 429;\n } else if (code === \"user_soft_blocked\" || code === \"prompt_abuse_detected\") status = 403;\n else if ([\"server_llm_key_missing\", \"user_llm_connection_required\", \"message_required\", \"missing_api_key\"].includes(code) || msg.includes(\"missing_api_key_for_\"))\n status = 400;\n let rhizohFailureKind = \"provider_error\";\n if (msg === \"rate_limit_exceeded\" || status === 429) rhizohFailureKind = \"rate_limit\";\n else if (status === 403) rhizohFailureKind = \"policy_block\";\n else if (status === 400) rhizohFailureKind = \"client_config\";\n else if (msg.startsWith(\"provider_http_\")) {\n const st = Number(msg.replace(\"provider_http_\", \"\"));\n if (st === 408 || st === 504) rhizohFailureKind = \"timeout\";\n else rhizohFailureKind = \"provider_error\";\n } else if (/timeout|timed out|ETIMEDOUT/i.test(msg)) rhizohFailureKind = \"timeout\";\n\n const providerHttpMatch = msg.match(/^provider_http_(\\d{3})$/);\n const stressTaxonomy =\n error?.stressClass != null\n ? {\n stressClass: error.stressClass,\n responseAction: error.responseAction,\n stressMatrix: error.stressMatrix,\n stressInterpretable: error.stressInterpretable !== false,\n stressConfidence: error.stressConfidence,\n actionConfidence: error.actionConfidence,\n actionSoftened: error.actionSoftened,\n responseActionStrict: error.responseActionStrict,\n actionInterpretable: error.actionInterpretable,\n conflictResolution: error.conflictResolution,\n stressSecondary: error.stressSecondary,\n responseActions: error.responseActions\n }\n : (() => {\n const t = classifyStressResponseV0({\n code: code || msg,\n rhizohFailureKind,\n providerHttpStatus: providerHttpMatch\n ? Number(providerHttpMatch[1])\n : null\n });\n return {\n stressClass: t.stressClass,\n responseAction: t.responseAction,\n stressMatrix: t.matrix,\n stressInterpretable: t.interpretable,\n stressConfidence: t.stressConfidence,\n actionConfidence: t.actionConfidence,\n actionSoftened: t.actionSoftened,\n responseActionStrict: t.responseActionStrict,\n actionInterpretable: t.actionInterpretable,\n conflictResolution: t.conflictResolution,\n stressSecondary: t.stressSecondary,\n responseActions: t.responseActions\n };\n })();\n sendJson(res, status, {\n ok: false,\n rhizohFailureKind,\n ...(providerHttpMatch ? { providerHttpStatus: Number(providerHttpMatch[1]) } : {}),\n error: code || msg || \"rhizoh_llm_failed\",\n reply:\n error?.reply ||\n (status === 500 ? \"Rhizoh bağlantısı geçici olarak kesildi.\" : msg),\n directive: error?.directive || \"NONE\",\n ...stressTaxonomy\n });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicSeal) {\n try {\n if (!EPISTEMIC_SEAL_SECRET) {\n sendJson(res, 503, { ok: false, error: \"epistemic_seal_disabled\" });\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const ip = getHttpClientIp(req);\n if (!checkHttpRateLimit(`epistemic_seal:${ip}`, RL_EPISTEMIC_SEAL_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 256 * 1024);\n const truth = body.truth_contract;\n if (!truth || typeof truth !== \"object\") {\n sendJson(res, 400, { ok: false, error: \"truth_contract_required\" });\n return;\n }\n const sealBody = {\n truth_contract: truth,\n runtime_hash: String(body.runtime_hash || \"\"),\n model_route: {\n provider: body.model_route?.provider ?? null,\n model: body.model_route?.model ?? null\n },\n memory_digest: String(body.memory_digest || \"\"),\n world_snapshot_hash: String(body.world_snapshot_hash || \"\"),\n timestamp: Number(body.timestamp) || Date.now()\n };\n const canonical = canonicalEpistemicSealString(sealBody);\n const { hash, signature } = hashAndSignEpistemicSeal(canonical, EPISTEMIC_SEAL_SECRET);\n recordGenesisEpistemicSealIssued(hash);\n const attestation = {\n schema: EPISTEMIC_SEAL_SCHEMA,\n algorithm: \"SHA-256+HMAC-SHA256\",\n keyHint: process.env.CASTLE_EPISTEMIC_SEAL_SECRET ? \"CASTLE_EPISTEMIC_SEAL_SECRET\" : \"CASTLE_GATEWAY_TOKEN\",\n issuedAt: Date.now(),\n gateway: { service: \"castle-gateway\", listenPort: PORT }\n };\n sendJson(res, 200, {\n ok: true,\n hash,\n signature,\n attestation,\n canonicalBytes: Buffer.byteLength(canonical, \"utf8\")\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"seal_failed\") });\n }\n return;\n }\n\n if (req.method === \"POST\" && pathname === rhizohRuntime.routes.epistemicLogsBatch) {\n try {\n if (REQUIRED_GATEWAY_TOKEN) {\n const tok = readGatewayHttpToken(req);\n if (tok !== REQUIRED_GATEWAY_TOKEN) {\n sendJson(res, 401, { ok: false, error: \"gateway_token_required\" });\n return;\n }\n }\n const auth = await resolveHttpUser(req);\n if (!auth.ok) {\n sendJson(res, 401, { ok: false, error: auth.reason || \"auth_required\" });\n return;\n }\n const ip = getHttpClientIp(req);\n const rlKey = `uid:${auth.uid || ip}`;\n if (!checkHttpRateLimit(`epistemic_logs:${rlKey}`, RL_EPISTEMIC_LOG_PER_MIN, 60_000)) {\n sendJson(res, 429, { ok: false, error: \"rate_limit_exceeded\" });\n return;\n }\n const body = await readHttpJson(req, 512 * 1024);\n const entries = Array.isArray(body?.entries) ? body.entries.slice(0, 120) : [];\n if (!entries.length) {\n sendJson(res, 400, { ok: false, error: \"entries_required\" });\n return;\n }\n const persisted = await persistEpistemicLedgerBatch(auth.uid, entries);\n recordGenesisEpistemicLedgerPersisted(persisted.persisted);\n // Internal cognition layer: forecast stays gateway/ledger-only (no client exposure).\n try {\n await persistEpistemicForecastBatch(auth.uid, persisted.normalized || []);\n } catch {\n /* forecast failures must not break primary ledger ingest */\n }\n sendJson(res, 200, {\n ok: true,\n persisted: persisted.persisted,\n mode: persisted.mode,\n latest: persisted.normalized?.[persisted.normalized.length - 1] || null\n });\n } catch (e) {\n sendJson(res, 400, { ok: false, error: String(e?.message || e || \"epistemic_log_ingest_failed\") });\n }\n return;\n }\n\n sendJson(res, 404, { ok: false, error: \"not_found\" });\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\nspiralWssForGenesis = wss;\nconst orchestrator = createOrchestrator();\nconst clientStats = new Map();\nlet broadcasterClientId = null;\nconst spiralState = {\n rooms: [{ id: \"room-default\", name: \"Spiral Default Arena\" }],\n activeByClient: new Map(),\n characters: new Map() // id -> {id,name,role,roomId,source}\n};\n\n/** Castle social rooms — keyed by `castleRoomKey` from {@link WS_MESSAGE.CASTLE_SOCIAL_PULSE}. */\nconst castleSocialRoomState = new Map();\nconst CASTLE_SOCIAL_MEMBER_TTL_MS = 90_000;\nconst CASTLE_SOCIAL_MAX_PULSE_BYTES = 8192;\n\nfunction removeCastleSocialClientFromAllRooms(clientId) {\n const cid = String(clientId || \"\");\n if (!cid) return;\n for (const room of castleSocialRoomState.values()) {\n room.members.delete(cid);\n }\n}\n\nfunction buildCastleSocialRoomPayload(roomKey) {\n const room = castleSocialRoomState.get(roomKey);\n if (!room) return null;\n const now = Date.now();\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const roster = [...room.members.values()].map((m) => ({\n userId: m.userId,\n lastMs: m.lastMs,\n ...m.sp\n }));\n const peerFeeds =\n room.walPeers && room.walPeers.size\n ? [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }))\n : [];\n return { castleRoomKey: roomKey, seq: room.seq, roster, peerFeeds, ts: now };\n}\n\nconst CASTLE_WAL_PEER_MAX_FEED_BYTES = 48 * 1024;\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {{ ok: boolean, code?: string, reason?: string }} gate\n */\nfunction rejectWalPeerIngressV0(socket, gate) {\n const code = gate.code || \"wal_peer_rejected\";\n recordWalPeerRejectV0(code);\n recordSubstrateOtelEventV0(\"wal_peer_reject\", { code, reason: String(gate.reason || \"\").slice(0, 120) });\n socket.send(\n JSON.stringify(\n createEnvelope(WS_MESSAGE.ERROR, {\n error: gate.reason || \"WAL peer ingress rejected.\",\n code\n })\n )\n );\n}\n\n/**\n * @param {import(\"ws\").WebSocket} socket\n * @param {Record<string, unknown> | null | undefined} walPeerFeed\n * @returns {boolean}\n */\nfunction gateWalPeerIngressV0(socket, walPeerFeed) {\n const authGate = assertWalPeerSocketAuthorityV0(socket);\n if (!authGate.ok) {\n rejectWalPeerIngressV0(socket, authGate);\n return false;\n }\n const sigGate = validateWalPeerFeedSignatureV0(\n walPeerFeed && typeof walPeerFeed === \"object\" ? walPeerFeed : {}\n );\n if (!sigGate.ok) {\n rejectWalPeerIngressV0(socket, sigGate);\n return false;\n }\n return true;\n}\n\nfunction handleCastleWalPeerFeed(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_WAL_PEER_MAX_FEED_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_WAL_PEER_FEED too large.\" })));\n return;\n }\n const walPeerFeed = p.walPeerFeed && typeof p.walPeerFeed === \"object\" ? p.walPeerFeed : {};\n if (!gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const castleRoomKey = String(p.castleRoomKey || \"wal:convergence\").slice(0, 64);\n const castleId = String(p.castleId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId,\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: room.members.get(socket.clientId)?.sp ?? {}\n });\n const peerFeeds = [...room.walPeers.values()].map((m) => ({\n castleId: m.castleId,\n userId: m.userId,\n lastMs: m.lastMs,\n walPeerFeed: m.walPeerFeed\n }));\n const body = {\n castleRoomKey,\n seq: room.seq,\n peerFeeds,\n ts: now\n };\n const env = createEnvelope(WS_MESSAGE.CASTLE_WAL_PEER_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys(), ...room.walPeers.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction handleCastleSocialPulse(socket, payload) {\n const p = payload && typeof payload === \"object\" ? payload : {};\n const probe = JSON.stringify(p);\n if (Buffer.byteLength(probe, \"utf8\") > CASTLE_SOCIAL_MAX_PULSE_BYTES) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"CASTLE_SOCIAL_PULSE too large.\" })));\n return;\n }\n const castleRoomKey = String(p.castleRoomKey || \"default\").slice(0, 64);\n const userId = String(p.userId || socket.auth?.user?.uid || socket.clientId || \"anonymous\").slice(0, 128);\n let room = castleSocialRoomState.get(castleRoomKey);\n if (!room) {\n room = { seq: 0, members: new Map(), walPeers: new Map() };\n castleSocialRoomState.set(castleRoomKey, room);\n }\n if (!room.walPeers) room.walPeers = new Map();\n room.seq += 1;\n const now = Date.now();\n const sp = p.socialPulse && typeof p.socialPulse === \"object\" ? p.socialPulse : {};\n const walPeerFeed =\n p.walPeerFeed && typeof p.walPeerFeed === \"object\"\n ? p.walPeerFeed\n : sp.walPeerFeed && typeof sp.walPeerFeed === \"object\"\n ? sp.walPeerFeed\n : null;\n if (walPeerFeed && !gateWalPeerIngressV0(socket, walPeerFeed)) return;\n const spNext = walPeerFeed ? { ...sp, walPeerFeed } : { ...sp };\n room.members.set(socket.clientId, {\n clientId: socket.clientId,\n userId,\n lastMs: now,\n sp: spNext\n });\n if (walPeerFeed) {\n room.walPeers.set(socket.clientId, {\n clientId: socket.clientId,\n castleId: String(p.castleId || userId).slice(0, 64),\n userId,\n lastMs: now,\n walPeerFeed: { ...walPeerFeed, observedAtMs: Number(walPeerFeed.observedAtMs) || now }\n });\n }\n for (const [cid, m] of [...room.members.entries()]) {\n if (now - m.lastMs > CASTLE_SOCIAL_MEMBER_TTL_MS) room.members.delete(cid);\n }\n const body = buildCastleSocialRoomPayload(castleRoomKey);\n if (!body) return;\n const env = createEnvelope(WS_MESSAGE.CASTLE_SOCIAL_ROOM, body);\n const encoded = JSON.stringify(env);\n const memberIds = new Set([...room.members.keys()]);\n for (const client of wss.clients) {\n if (memberIds.has(client.clientId) && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcast(msgObj) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction broadcastState() {\n broadcast(\n createEnvelope(WS_MESSAGE.BROADCAST_STATE, {\n broadcasterClientId,\n viewers: [...wss.clients].filter((c) => c.readyState === 1).length\n })\n );\n}\n\nfunction currentSpiralStatePayload() {\n return {\n rooms: spiralState.rooms,\n characters: [...spiralState.characters.values()]\n };\n}\n\nfunction broadcastSpiralState() {\n broadcast(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload()));\n}\n\nfunction ensureRoom(roomId, fallbackName = null) {\n if (!roomId) return spiralState.rooms[0].id;\n const exists = spiralState.rooms.find((r) => r.id === roomId);\n if (exists) return exists.id;\n const created = { id: roomId, name: fallbackName || `Spiral-${roomId.slice(-4)}` };\n spiralState.rooms.push(created);\n return created.id;\n}\n\nfunction syncSpiralCharactersWithWorld(snapshot) {\n const agents = snapshot?.agents || [];\n for (const a of agents) {\n if (!spiralState.characters.has(a.id)) {\n spiralState.characters.set(a.id, {\n id: a.id,\n name: a.id,\n role: \"agent\",\n roomId: spiralState.rooms[0].id,\n source: \"world\"\n });\n }\n }\n}\n\nfunction broadcastExcept(msgObj, exceptSocket) {\n const encoded = JSON.stringify(msgObj);\n for (const client of wss.clients) {\n if (client !== exceptSocket && client.readyState === 1) client.send(encoded);\n }\n}\n\nfunction validateInputFrame(payload, socket) {\n const stats = clientStats.get(socket.clientId) || { lastFrame: 0, windowStart: Date.now(), msgCount: 0 };\n const now = Date.now();\n if (now - stats.windowStart > 5000) {\n stats.windowStart = now;\n stats.msgCount = 0;\n }\n stats.msgCount += 1;\n clientStats.set(socket.clientId, stats);\n if (stats.msgCount > 120) return \"Rate limit exceeded.\";\n\n if (!payload || !Number.isInteger(payload.frame)) return \"frame must be integer.\";\n if (payload.frame <= stats.lastFrame) return \"frame must be monotonic.\";\n const commands = Array.isArray(payload.commands) ? payload.commands : [];\n if (commands.length > 8) return \"Too many commands in one frame.\";\n for (const cmd of commands) {\n if (!cmd || typeof cmd.command !== \"string\") return \"Invalid command format.\";\n if (\n cmd.command !== \"SPAWN_AGENT\" &&\n cmd.command !== \"LIST_CASTLES\" &&\n cmd.command !== \"SPAWN_ENTITY\" &&\n cmd.command !== \"CREATE_CASTLE\"\n ) return \"Unsupported command.\";\n }\n stats.lastFrame = payload.frame;\n clientStats.set(socket.clientId, stats);\n return null;\n}\n\nfunction validateSignalPayload(payload) {\n if (!payload || typeof payload !== \"object\") return \"Invalid signaling payload.\";\n const signalType = payload.signalType;\n if (![\"OFFER\", \"ANSWER\", \"ICE\"].includes(signalType)) return \"Invalid signal type.\";\n if (typeof payload.to !== \"string\" || payload.to.length < 3) return \"Invalid signal destination.\";\n if (signalType !== \"ICE\" && JSON.stringify(payload.sdp || {}).length > 16 * 1024) return \"SDP too large.\";\n if (signalType === \"ICE\" && JSON.stringify(payload.candidate || {}).length > 6 * 1024) return \"ICE too large.\";\n return null;\n}\n\nfunction sanitizeRawMessage(raw) {\n if (!raw) return null;\n const text = raw.toString();\n if (Buffer.byteLength(text, \"utf8\") > MAX_MESSAGE_BYTES) return null;\n return text;\n}\n\nwss.on(\"connection\", async (socket, req) => {\n const origin = req?.headers?.origin || \"\";\n if (ALLOWED_ORIGINS.length && !ALLOWED_ORIGINS.includes(origin)) {\n socket.close(1008, \"Origin not allowed\");\n return;\n }\n if (REQUIRED_GATEWAY_TOKEN) {\n const path = req?.url || \"\";\n const query = new URL(`ws://localhost${path}`).searchParams;\n const token = query.get(\"token\");\n if (!token || token !== REQUIRED_GATEWAY_TOKEN) {\n socket.close(1008, \"Invalid gateway token\");\n return;\n }\n }\n if (REQUIRE_AUTH) {\n const result = await verifyClientToken(req);\n if (!result.ok) {\n const isLocal = origin.includes(\"localhost\") || origin.includes(\"127.0.0.1\") || !origin;\n if (!(ALLOW_DEV_ANON && isLocal)) {\n socket.close(1008, result.reason);\n return;\n }\n socket.auth = { ok: true, kind: \"dev-anon\", user: { uid: \"dev-anon\" } };\n } else {\n socket.auth = result;\n }\n }\n\n socket.clientId = `c-${Math.random().toString(36).slice(2, 8)}`;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.HELLO, { message: \"Castle Gateway online.\", clientId: socket.clientId })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.WORLD_SNAPSHOT, orchestrator.worldSnapshot())));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.BROADCAST_STATE, { broadcasterClientId, viewers: [...wss.clients].filter((c) => c.readyState === 1).length })));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.SPIRAL_STATE, currentSpiralStatePayload())));\n broadcastExcept(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }), socket);\n\n socket.on(\"message\", (raw) => {\n const safeRaw = sanitizeRawMessage(raw);\n if (!safeRaw) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Message too large or invalid.\" })));\n return;\n }\n const parsed = safeJsonParse(safeRaw);\n if (!parsed?.type) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid message.\" })));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND_TEXT) {\n const canonical = parseTextToCommand(parsed.payload?.text);\n if (!canonical) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Unknown command text.\" })));\n return;\n }\n const result = orchestrator.applyCommand(canonical);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.COMMAND) {\n const result = orchestrator.applyCommand(parsed.payload);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, result)));\n }\n\n if (parsed.type === WS_MESSAGE.INPUT_FRAME) {\n const err = validateInputFrame(parsed.payload, socket);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const frame = parsed.payload.frame;\n const commands = Array.isArray(parsed.payload?.commands) ? parsed.payload.commands : [];\n orchestrator.queueInputFrame(socket.clientId, frame, commands);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, queued: true, frame })));\n }\n\n if (parsed.type === WS_MESSAGE.SIGNAL) {\n const payload = parsed.payload || {};\n const err = validateSignalPayload(payload);\n if (err) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: err })));\n return;\n }\n const signalType = payload.signalType;\n const to = payload.to;\n for (const client of wss.clients) {\n if (client.clientId === to && client.readyState === 1) {\n client.send(JSON.stringify(createEnvelope(WS_MESSAGE.SIGNAL, { ...payload, from: socket.clientId })));\n break;\n }\n }\n }\n\n if (parsed.type === WS_MESSAGE.DRONE_TELEMETRY) {\n const payload = parsed.payload || {};\n const json = JSON.stringify(payload);\n if (Buffer.byteLength(json, \"utf8\") > 48 * 1024) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"DRONE_TELEMETRY payload too large.\" })));\n return;\n }\n broadcastExcept(createEnvelope(WS_MESSAGE.DRONE_TELEMETRY, { ...payload, relayFrom: socket.clientId }), socket);\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_SOCIAL_PULSE) {\n handleCastleSocialPulse(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.CASTLE_WAL_PEER_FEED) {\n handleCastleWalPeerFeed(socket, parsed.payload || {});\n return;\n }\n\n if (parsed.type === WS_MESSAGE.OPEN_DATA_QUERY) {\n queryOpenData(parsed.payload)\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: true, result })));\n })\n .catch((e) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.OPEN_DATA_RESULT, { ok: false, error: e.message })));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.RHIZOH_QUERY) {\n runRhizohBrainV2({\n ...(parsed.payload || {}),\n uid: socket?.auth?.user?.uid || \"unknown\"\n })\n .then((result) => {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, result)));\n })\n .catch(async () => {\n const fallback = await runRhizohBrain(parsed.payload || {}).catch(() => ({\n ok: false,\n reply: \"Rhizoh temporary disturbance. Habitat remains stable.\"\n }));\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.RHIZOH_RESULT, fallback)));\n });\n return;\n }\n\n if (parsed.type === WS_MESSAGE.BROADCAST_REGISTER) {\n const role = String(parsed.payload?.role || \"\");\n if (role === \"GENESIS_BROADCAST_AGENT\") {\n broadcasterClientId = socket.clientId;\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, registered: role })));\n broadcastState();\n }\n return;\n }\n\n if (parsed.type === WS_MESSAGE.STUDIO_CUE) {\n // Only registered broadcaster can push global cues.\n if (!broadcasterClientId || socket.clientId !== broadcasterClientId) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Only broadcaster agent can emit STUDIO_CUE.\" })));\n return;\n }\n const cue = parsed.payload || {};\n if (typeof cue !== \"object\") {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Invalid STUDIO_CUE payload.\" })));\n return;\n }\n broadcast(createEnvelope(WS_MESSAGE.STUDIO_CUE, cue));\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_JOIN_ROOM) {\n const requested = String(parsed.payload?.roomId || \"\").trim();\n const requestedName = String(parsed.payload?.roomName || \"\").trim();\n const roomId = ensureRoom(requested || `room-${Math.random().toString(36).slice(2, 7)}`, requestedName || null);\n spiralState.activeByClient.set(socket.clientId, roomId);\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, roomId })));\n broadcastSpiralState();\n return;\n }\n\n if (parsed.type === WS_MESSAGE.SPIRAL_CREATE_CHARACTER) {\n const name = String(parsed.payload?.name || \"\").trim().slice(0, 32) || `AI-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;\n const role = String(parsed.payload?.role || \"agent\").trim().slice(0, 24) || \"agent\";\n const roomId = ensureRoom(\n String(parsed.payload?.roomId || spiralState.activeByClient.get(socket.clientId) || spiralState.rooms[0].id),\n null\n );\n\n // Character creation always maps to an authoritative world agent.\n const result = orchestrator.applyCommand({\n command: COMMAND.SPAWN_AGENT,\n params: { targetCastleId: \"castle-01\" }\n });\n if (!result?.ok || !result?.data?.id) {\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.ERROR, { error: \"Character creation failed.\" })));\n return;\n }\n const id = result.data.id;\n spiralState.characters.set(id, { id, name, role, roomId, source: \"spawned\" });\n socket.send(JSON.stringify(createEnvelope(WS_MESSAGE.COMMAND_RESULT, { ok: true, characterId: id, roomId })));\n broadcastSpiralState();\n return;\n }\n });\n\n socket.on(\"close\", () => {\n if (socket.clientId === broadcasterClientId) broadcasterClientId = null;\n clientStats.delete(socket.clientId);\n spiralState.activeByClient.delete(socket.clientId);\n removeCastleSocialClientFromAllRooms(socket.clientId);\n broadcast(createEnvelope(WS_MESSAGE.PEERS, { peers: [...wss.clients].map((c) => c.clientId).filter(Boolean) }));\n broadcastState();\n });\n});\n\nsetInterval(() => {\n void (async () => {\n try {\n const { frame, snapshot, delta, packedDelta } = orchestrator.tick(1 / 20);\n syncSpiralCharactersWithWorld(snapshot);\n broadcast(createEnvelope(WS_MESSAGE.NETWORK_TICK, { frame }));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_TICK, snapshot));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA, delta));\n broadcast(createEnvelope(WS_MESSAGE.WORLD_DELTA_PACKED, packedDelta));\n broadcastSpiralState();\n await runAcademyEventTick();\n const executed = await tickAutomations(Date.now());\n for (const a of executed) {\n await addTranscript(a.uid, {\n source: \"planner\",\n eventType: \"automation-executed\",\n text: `Automation executed: ${a.title} (${a.action})`,\n roomId: \"studio-main\",\n meta: { automationId: a.id, action: a.action, deviceId: a.deviceId || \"\" }\n });\n }\n } catch (e) {\n console.error(\"[GATEWAY] world tick / automations loop error:\", String(e?.message || e));\n }\n })();\n}, 50);\n\n(async () => {\n await rhizohRuntime.telemetry.initOpenTelemetry();\n if (String(process.env.CASTLE_GENESIS_DISK_PERSIST ?? \"\").trim() === \"\") {\n process.env.CASTLE_GENESIS_DISK_PERSIST = \"1\";\n }\n const genesisDiskBoot = await hydrateGenesisContinuityPersistenceBootV0();\n if (!genesisDiskBoot.ok) {\n console.warn(\"[GATEWAY] genesis disk hydrate failed; continuing with in-memory genesis continuity\");\n }\n httpServer.listen(PORT, () => {\n startGenesisCanonicalClock();\n installGenesisCheckpointSurfaceGetter(buildGenesisRuntimeSurfacePayloadLive);\n setGenesisContinuityAfterPublishHook((seq) => {\n noteGenesisCheckpointSeqCommitted(seq);\n noteGenesisContinuitySeqAcceptedV0(seq);\n });\n startGenesisContinuityInfraSampler(buildGenesisRuntimeSurfacePayloadLive, 2000);\n logProductionObservatorySurfaceGuardsV0();\n logGatewaySubstrateAuthorityGuardsV0();\n console.log(`[GATEWAY] ws/http://localhost:${PORT}`);\n console.log(\n `[GATEWAY] genesis continuity: GET ${rhizohRuntime.routes.genesisRuntime} | POST ${rhizohRuntime.routes.genesisIngress} | SSE ${rhizohRuntime.routes.genesisStream} | replay ${rhizohRuntime.routes.genesisReplay} | diff ${rhizohRuntime.routes.genesisReplayDiff} | equiv ${rhizohRuntime.routes.genesisReplayEquivalence} | analytics ${rhizohRuntime.routes.genesisReplayAnalytics} | evolution ${rhizohRuntime.routes.genesisReplayEvolution}`\n );\n if (String(process.env.CASTLE_GENESIS_OBSERVATORY_LIVE_V01 ?? \"\").trim() === \"1\") {\n console.log(\"GENESIS OBSERVATORY LIVE v0.1 — READ ONLY PHASE STARTED\");\n }\n });\n})().catch((e) => {\n console.error(\"[GATEWAY] fatal boot (before or during listen):\", e);\n process.exit(1);\n});\n"}}}} |