Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/cankasaplar_castle-genesis-rhizoh-habitatos.json
T

1 line
5.2 MiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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"}}}}