paracosm 0.4.346 → 0.4.377

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (671) hide show
  1. package/README.md +186 -14
  2. package/config/leaders.example.json +32 -0
  3. package/config/leaders.json +32 -0
  4. package/dist/cli/cli-run-options.d.ts +11 -0
  5. package/dist/cli/cli-run-options.d.ts.map +1 -0
  6. package/dist/cli/cli-run-options.js +39 -0
  7. package/dist/cli/cli-run-options.js.map +1 -0
  8. package/dist/cli/compile-cli-options.d.ts +14 -0
  9. package/dist/cli/compile-cli-options.d.ts.map +1 -0
  10. package/dist/cli/compile-cli-options.js +48 -0
  11. package/dist/cli/compile-cli-options.js.map +1 -0
  12. package/dist/cli/compile.d.ts +11 -0
  13. package/dist/cli/compile.d.ts.map +1 -0
  14. package/dist/cli/compile.js +92 -0
  15. package/dist/cli/compile.js.map +1 -0
  16. package/dist/cli/custom-scenarios.d.ts +10 -0
  17. package/dist/cli/custom-scenarios.d.ts.map +1 -0
  18. package/dist/cli/custom-scenarios.js +93 -0
  19. package/dist/cli/custom-scenarios.js.map +1 -0
  20. package/dist/cli/leaders-resolver.d.ts +26 -0
  21. package/dist/cli/leaders-resolver.d.ts.map +1 -0
  22. package/dist/cli/leaders-resolver.js +66 -0
  23. package/dist/cli/leaders-resolver.js.map +1 -0
  24. package/dist/cli/pair-runner.d.ts +23 -0
  25. package/dist/cli/pair-runner.d.ts.map +1 -0
  26. package/dist/cli/pair-runner.js +225 -0
  27. package/dist/cli/pair-runner.js.map +1 -0
  28. package/dist/cli/rate-limiter.d.ts +78 -0
  29. package/dist/cli/rate-limiter.d.ts.map +1 -0
  30. package/dist/cli/rate-limiter.js +193 -0
  31. package/dist/cli/rate-limiter.js.map +1 -0
  32. package/dist/cli/retry-stats.d.ts +179 -0
  33. package/dist/cli/retry-stats.d.ts.map +1 -0
  34. package/dist/cli/retry-stats.js +167 -0
  35. package/dist/cli/retry-stats.js.map +1 -0
  36. package/dist/cli/run-a.d.ts +2 -0
  37. package/dist/cli/run-a.d.ts.map +1 -0
  38. package/dist/cli/run-a.js +13 -0
  39. package/dist/cli/run-a.js.map +1 -0
  40. package/dist/cli/run-b.d.ts +2 -0
  41. package/dist/cli/run-b.d.ts.map +1 -0
  42. package/dist/cli/run-b.js +13 -0
  43. package/dist/cli/run-b.js.map +1 -0
  44. package/dist/cli/run.d.ts +18 -0
  45. package/dist/cli/run.d.ts.map +1 -0
  46. package/dist/cli/run.js +135 -0
  47. package/dist/cli/run.js.map +1 -0
  48. package/dist/cli/serve.d.ts +10 -0
  49. package/dist/cli/serve.d.ts.map +1 -0
  50. package/dist/cli/serve.js +71 -0
  51. package/dist/cli/serve.js.map +1 -0
  52. package/dist/cli/server/routes/platform-api.d.ts +8 -0
  53. package/dist/cli/server/routes/platform-api.d.ts.map +1 -0
  54. package/dist/cli/server/routes/platform-api.js +41 -0
  55. package/dist/cli/server/routes/platform-api.js.map +1 -0
  56. package/dist/cli/server/routes/public-demo.d.ts +4 -0
  57. package/dist/cli/server/routes/public-demo.d.ts.map +1 -0
  58. package/dist/cli/server/routes/public-demo.js +16 -0
  59. package/dist/cli/server/routes/public-demo.js.map +1 -0
  60. package/dist/cli/server/run-history-store.d.ts +8 -0
  61. package/dist/cli/server/run-history-store.d.ts.map +1 -0
  62. package/dist/cli/server/run-history-store.js +8 -0
  63. package/dist/cli/server/run-history-store.js.map +1 -0
  64. package/dist/cli/server/run-record.d.ts +14 -0
  65. package/dist/cli/server/run-record.d.ts.map +1 -0
  66. package/dist/cli/server/run-record.js +12 -0
  67. package/dist/cli/server/run-record.js.map +1 -0
  68. package/dist/cli/server/server-mode.d.ts +3 -0
  69. package/dist/cli/server/server-mode.d.ts.map +1 -0
  70. package/dist/cli/server/server-mode.js +8 -0
  71. package/dist/cli/server/server-mode.js.map +1 -0
  72. package/dist/cli/server-app.d.ts +46 -0
  73. package/dist/cli/server-app.d.ts.map +1 -0
  74. package/dist/cli/server-app.js +1846 -0
  75. package/dist/cli/server-app.js.map +1 -0
  76. package/dist/cli/session-store.d.ts +103 -0
  77. package/dist/cli/session-store.d.ts.map +1 -0
  78. package/dist/cli/session-store.js +254 -0
  79. package/dist/cli/session-store.js.map +1 -0
  80. package/dist/cli/session-title.d.ts +81 -0
  81. package/dist/cli/session-title.d.ts.map +1 -0
  82. package/dist/cli/session-title.js +229 -0
  83. package/dist/cli/session-title.js.map +1 -0
  84. package/dist/cli/sim-config.d.ts.map +1 -0
  85. package/dist/cli/sim-config.js.map +1 -0
  86. package/dist/cli/types.d.ts.map +1 -0
  87. package/dist/{src/cli → cli}/types.js.map +1 -1
  88. package/dist/engine/_template/index.d.ts.map +1 -0
  89. package/dist/engine/_template/index.js.map +1 -0
  90. package/dist/engine/compiler/cache.d.ts.map +1 -0
  91. package/dist/engine/compiler/cache.js.map +1 -0
  92. package/dist/engine/compiler/generate-director.d.ts.map +1 -0
  93. package/dist/engine/compiler/generate-director.js.map +1 -0
  94. package/dist/engine/compiler/generate-fingerprint.d.ts.map +1 -0
  95. package/dist/engine/compiler/generate-fingerprint.js.map +1 -0
  96. package/dist/engine/compiler/generate-milestones.d.ts.map +1 -0
  97. package/dist/engine/compiler/generate-milestones.js.map +1 -0
  98. package/dist/engine/compiler/generate-politics.d.ts.map +1 -0
  99. package/dist/engine/compiler/generate-politics.js.map +1 -0
  100. package/dist/engine/compiler/generate-progression.d.ts.map +1 -0
  101. package/dist/engine/compiler/generate-progression.js.map +1 -0
  102. package/dist/engine/compiler/generate-prompts.d.ts.map +1 -0
  103. package/dist/engine/compiler/generate-prompts.js.map +1 -0
  104. package/dist/engine/compiler/generate-reactions.d.ts.map +1 -0
  105. package/dist/engine/compiler/generate-reactions.js.map +1 -0
  106. package/dist/engine/compiler/index.d.ts.map +1 -0
  107. package/dist/engine/compiler/index.js.map +1 -0
  108. package/dist/engine/compiler/llm-invocations/generateValidatedCode.d.ts.map +1 -0
  109. package/dist/engine/compiler/llm-invocations/generateValidatedCode.js.map +1 -0
  110. package/dist/engine/compiler/llm-invocations/generateValidatedJson.d.ts.map +1 -0
  111. package/dist/engine/compiler/llm-invocations/generateValidatedJson.js.map +1 -0
  112. package/dist/engine/compiler/llm-invocations/generateValidatedObject.d.ts.map +1 -0
  113. package/dist/engine/compiler/llm-invocations/generateValidatedObject.js.map +1 -0
  114. package/dist/engine/compiler/llm-invocations/generateValidatedProse.d.ts.map +1 -0
  115. package/dist/engine/compiler/llm-invocations/generateValidatedProse.js.map +1 -0
  116. package/dist/engine/compiler/schemas/index.d.ts.map +1 -0
  117. package/dist/engine/compiler/schemas/index.js.map +1 -0
  118. package/dist/engine/compiler/schemas/milestones.d.ts.map +1 -0
  119. package/dist/engine/compiler/schemas/milestones.js.map +1 -0
  120. package/dist/engine/compiler/seed-ingestion.d.ts.map +1 -0
  121. package/dist/engine/compiler/seed-ingestion.js.map +1 -0
  122. package/dist/engine/compiler/telemetry.d.ts.map +1 -0
  123. package/dist/engine/compiler/telemetry.js.map +1 -0
  124. package/dist/engine/compiler/types.d.ts.map +1 -0
  125. package/dist/engine/compiler/types.js.map +1 -0
  126. package/dist/engine/compiler/validate.d.ts.map +1 -0
  127. package/dist/engine/compiler/validate.js.map +1 -0
  128. package/dist/engine/core/agent-generator.d.ts.map +1 -0
  129. package/dist/engine/core/agent-generator.js.map +1 -0
  130. package/dist/engine/core/kernel.d.ts.map +1 -0
  131. package/dist/engine/core/kernel.js.map +1 -0
  132. package/dist/engine/core/progression.d.ts.map +1 -0
  133. package/dist/engine/core/progression.js.map +1 -0
  134. package/dist/engine/core/rng.d.ts.map +1 -0
  135. package/dist/engine/core/rng.js.map +1 -0
  136. package/dist/engine/core/state.d.ts.map +1 -0
  137. package/dist/engine/core/state.js.map +1 -0
  138. package/dist/engine/effect-registry.d.ts.map +1 -0
  139. package/dist/engine/effect-registry.js.map +1 -0
  140. package/dist/engine/event-taxonomy.d.ts.map +1 -0
  141. package/dist/engine/event-taxonomy.js.map +1 -0
  142. package/dist/{src/engine → engine}/index.d.ts +2 -0
  143. package/dist/engine/index.d.ts.map +1 -0
  144. package/dist/{src/engine → engine}/index.js +5 -0
  145. package/dist/engine/index.js.map +1 -0
  146. package/dist/engine/lunar/effects.d.ts.map +1 -0
  147. package/dist/engine/lunar/effects.js.map +1 -0
  148. package/dist/engine/lunar/events.d.ts.map +1 -0
  149. package/dist/engine/lunar/events.js.map +1 -0
  150. package/dist/engine/lunar/fingerprint.d.ts.map +1 -0
  151. package/dist/engine/lunar/fingerprint.js.map +1 -0
  152. package/dist/engine/lunar/hooks.d.ts.map +1 -0
  153. package/dist/engine/lunar/hooks.js.map +1 -0
  154. package/dist/engine/lunar/index.d.ts.map +1 -0
  155. package/dist/engine/lunar/index.js.map +1 -0
  156. package/dist/engine/lunar/metrics.d.ts.map +1 -0
  157. package/dist/engine/lunar/metrics.js.map +1 -0
  158. package/dist/engine/lunar/milestones.d.ts.map +1 -0
  159. package/dist/engine/lunar/milestones.js.map +1 -0
  160. package/dist/engine/lunar/names.d.ts.map +1 -0
  161. package/dist/engine/lunar/names.js.map +1 -0
  162. package/dist/engine/lunar/politics.d.ts.map +1 -0
  163. package/dist/engine/lunar/politics.js.map +1 -0
  164. package/dist/engine/lunar/presets.d.ts.map +1 -0
  165. package/dist/engine/lunar/presets.js.map +1 -0
  166. package/dist/engine/lunar/progression-hooks.d.ts.map +1 -0
  167. package/dist/engine/lunar/progression-hooks.js.map +1 -0
  168. package/dist/engine/lunar/prompts.d.ts.map +1 -0
  169. package/dist/engine/lunar/prompts.js.map +1 -0
  170. package/dist/engine/lunar/reactions.d.ts.map +1 -0
  171. package/dist/engine/lunar/reactions.js.map +1 -0
  172. package/dist/engine/lunar/research-bundle.d.ts.map +1 -0
  173. package/dist/engine/lunar/research-bundle.js.map +1 -0
  174. package/dist/engine/mars/effects.d.ts.map +1 -0
  175. package/dist/engine/mars/effects.js.map +1 -0
  176. package/dist/engine/mars/events.d.ts.map +1 -0
  177. package/dist/engine/mars/events.js.map +1 -0
  178. package/dist/engine/mars/fingerprint.d.ts.map +1 -0
  179. package/dist/engine/mars/fingerprint.js.map +1 -0
  180. package/dist/engine/mars/hooks.d.ts.map +1 -0
  181. package/dist/engine/mars/hooks.js.map +1 -0
  182. package/dist/engine/mars/index.d.ts.map +1 -0
  183. package/dist/engine/mars/index.js.map +1 -0
  184. package/dist/engine/mars/metrics.d.ts.map +1 -0
  185. package/dist/engine/mars/metrics.js.map +1 -0
  186. package/dist/engine/mars/milestones.d.ts.map +1 -0
  187. package/dist/engine/mars/milestones.js.map +1 -0
  188. package/dist/engine/mars/names.d.ts.map +1 -0
  189. package/dist/engine/mars/names.js.map +1 -0
  190. package/dist/engine/mars/politics.d.ts.map +1 -0
  191. package/dist/engine/mars/politics.js.map +1 -0
  192. package/dist/engine/mars/presets.d.ts.map +1 -0
  193. package/dist/engine/mars/presets.js.map +1 -0
  194. package/dist/engine/mars/progression-hooks.d.ts.map +1 -0
  195. package/dist/engine/mars/progression-hooks.js.map +1 -0
  196. package/dist/engine/mars/prompts.d.ts.map +1 -0
  197. package/dist/engine/mars/prompts.js.map +1 -0
  198. package/dist/engine/mars/reactions.d.ts.map +1 -0
  199. package/dist/engine/mars/reactions.js.map +1 -0
  200. package/dist/engine/mars/research-bundle.d.ts.map +1 -0
  201. package/dist/engine/mars/research-bundle.js.map +1 -0
  202. package/dist/engine/metric-registry.d.ts.map +1 -0
  203. package/dist/engine/metric-registry.js.map +1 -0
  204. package/dist/engine/provider-resolver.d.ts.map +1 -0
  205. package/dist/{src/engine → engine}/provider-resolver.js +2 -1
  206. package/dist/engine/provider-resolver.js.map +1 -0
  207. package/dist/engine/types.d.ts.map +1 -0
  208. package/dist/{src/engine → engine}/types.js.map +1 -1
  209. package/dist/runtime/agent-memory.d.ts.map +1 -0
  210. package/dist/runtime/agent-memory.js.map +1 -0
  211. package/dist/runtime/agent-reactions.d.ts.map +1 -0
  212. package/dist/runtime/agent-reactions.js.map +1 -0
  213. package/dist/runtime/batch.d.ts.map +1 -0
  214. package/dist/runtime/batch.js.map +1 -0
  215. package/dist/runtime/chat-agents.d.ts.map +1 -0
  216. package/dist/runtime/chat-agents.js.map +1 -0
  217. package/dist/runtime/citations-catalog.d.ts.map +1 -0
  218. package/dist/runtime/citations-catalog.js.map +1 -0
  219. package/dist/runtime/commander-setup.d.ts.map +1 -0
  220. package/dist/runtime/commander-setup.js.map +1 -0
  221. package/dist/runtime/contracts.d.ts.map +1 -0
  222. package/dist/{src/runtime → runtime}/contracts.js.map +1 -1
  223. package/dist/runtime/cost-tracker.d.ts.map +1 -0
  224. package/dist/runtime/cost-tracker.js.map +1 -0
  225. package/dist/runtime/departments.d.ts.map +1 -0
  226. package/dist/runtime/departments.js.map +1 -0
  227. package/dist/runtime/director.d.ts.map +1 -0
  228. package/dist/runtime/director.js.map +1 -0
  229. package/dist/runtime/economics-profile.d.ts.map +1 -0
  230. package/dist/runtime/economics-profile.js.map +1 -0
  231. package/dist/runtime/emergent-setup.d.ts.map +1 -0
  232. package/dist/runtime/emergent-setup.js.map +1 -0
  233. package/dist/runtime/generic-fingerprint.d.ts.map +1 -0
  234. package/dist/runtime/generic-fingerprint.js.map +1 -0
  235. package/dist/runtime/hexaco-cues/trajectory.d.ts.map +1 -0
  236. package/dist/runtime/hexaco-cues/trajectory.js.map +1 -0
  237. package/dist/runtime/hexaco-cues/translation.d.ts.map +1 -0
  238. package/dist/runtime/hexaco-cues/translation.js.map +1 -0
  239. package/dist/runtime/index.d.ts.map +1 -0
  240. package/dist/runtime/index.js.map +1 -0
  241. package/dist/runtime/llm-invocations/generateValidatedObject.d.ts.map +1 -0
  242. package/dist/runtime/llm-invocations/generateValidatedObject.js.map +1 -0
  243. package/dist/runtime/llm-invocations/sendAndValidate.d.ts.map +1 -0
  244. package/dist/runtime/llm-invocations/sendAndValidate.js.map +1 -0
  245. package/dist/runtime/orchestrator.d.ts.map +1 -0
  246. package/dist/runtime/orchestrator.js.map +1 -0
  247. package/dist/runtime/output-writer.d.ts.map +1 -0
  248. package/dist/runtime/output-writer.js.map +1 -0
  249. package/dist/runtime/parsers.d.ts.map +1 -0
  250. package/dist/runtime/parsers.js.map +1 -0
  251. package/dist/runtime/pricing.d.ts.map +1 -0
  252. package/dist/runtime/pricing.js.map +1 -0
  253. package/dist/runtime/provider-errors.d.ts.map +1 -0
  254. package/dist/runtime/provider-errors.js.map +1 -0
  255. package/dist/runtime/reaction-step.d.ts.map +1 -0
  256. package/dist/runtime/reaction-step.js.map +1 -0
  257. package/dist/runtime/research/knowledge-base.d.ts.map +1 -0
  258. package/dist/runtime/research/knowledge-base.js.map +1 -0
  259. package/dist/runtime/research/research-memory.d.ts.map +1 -0
  260. package/dist/runtime/research/research-memory.js.map +1 -0
  261. package/dist/runtime/research/research.d.ts.map +1 -0
  262. package/dist/runtime/research/research.js.map +1 -0
  263. package/dist/runtime/research/scenario-research.d.ts.map +1 -0
  264. package/dist/runtime/research/scenario-research.js.map +1 -0
  265. package/dist/runtime/research/scenarios.d.ts.map +1 -0
  266. package/dist/runtime/research/scenarios.js.map +1 -0
  267. package/dist/runtime/runtime-helpers.d.ts.map +1 -0
  268. package/dist/runtime/runtime-helpers.js.map +1 -0
  269. package/dist/runtime/schemas/commander.d.ts.map +1 -0
  270. package/dist/runtime/schemas/commander.js.map +1 -0
  271. package/dist/runtime/schemas/department.d.ts.map +1 -0
  272. package/dist/runtime/schemas/department.js.map +1 -0
  273. package/dist/runtime/schemas/director.d.ts.map +1 -0
  274. package/dist/runtime/schemas/director.js.map +1 -0
  275. package/dist/runtime/schemas/index.d.ts.map +1 -0
  276. package/dist/runtime/schemas/index.js.map +1 -0
  277. package/dist/runtime/schemas/reactions.d.ts.map +1 -0
  278. package/dist/runtime/schemas/reactions.js.map +1 -0
  279. package/dist/runtime/schemas/verdict.d.ts.map +1 -0
  280. package/dist/runtime/schemas/verdict.js.map +1 -0
  281. package/dist/runtime/tool-ledger.d.ts.map +1 -0
  282. package/dist/runtime/tool-ledger.js.map +1 -0
  283. package/package.json +8 -2
  284. package/dist/src/cli/sim-config.d.ts.map +0 -1
  285. package/dist/src/cli/sim-config.js.map +0 -1
  286. package/dist/src/cli/types.d.ts.map +0 -1
  287. package/dist/src/engine/_template/index.d.ts.map +0 -1
  288. package/dist/src/engine/_template/index.js.map +0 -1
  289. package/dist/src/engine/compiler/cache.d.ts.map +0 -1
  290. package/dist/src/engine/compiler/cache.js.map +0 -1
  291. package/dist/src/engine/compiler/generate-director.d.ts.map +0 -1
  292. package/dist/src/engine/compiler/generate-director.js.map +0 -1
  293. package/dist/src/engine/compiler/generate-fingerprint.d.ts.map +0 -1
  294. package/dist/src/engine/compiler/generate-fingerprint.js.map +0 -1
  295. package/dist/src/engine/compiler/generate-milestones.d.ts.map +0 -1
  296. package/dist/src/engine/compiler/generate-milestones.js.map +0 -1
  297. package/dist/src/engine/compiler/generate-politics.d.ts.map +0 -1
  298. package/dist/src/engine/compiler/generate-politics.js.map +0 -1
  299. package/dist/src/engine/compiler/generate-progression.d.ts.map +0 -1
  300. package/dist/src/engine/compiler/generate-progression.js.map +0 -1
  301. package/dist/src/engine/compiler/generate-prompts.d.ts.map +0 -1
  302. package/dist/src/engine/compiler/generate-prompts.js.map +0 -1
  303. package/dist/src/engine/compiler/generate-reactions.d.ts.map +0 -1
  304. package/dist/src/engine/compiler/generate-reactions.js.map +0 -1
  305. package/dist/src/engine/compiler/index.d.ts.map +0 -1
  306. package/dist/src/engine/compiler/index.js.map +0 -1
  307. package/dist/src/engine/compiler/llm-invocations/generateValidatedCode.d.ts.map +0 -1
  308. package/dist/src/engine/compiler/llm-invocations/generateValidatedCode.js.map +0 -1
  309. package/dist/src/engine/compiler/llm-invocations/generateValidatedJson.d.ts.map +0 -1
  310. package/dist/src/engine/compiler/llm-invocations/generateValidatedJson.js.map +0 -1
  311. package/dist/src/engine/compiler/llm-invocations/generateValidatedObject.d.ts.map +0 -1
  312. package/dist/src/engine/compiler/llm-invocations/generateValidatedObject.js.map +0 -1
  313. package/dist/src/engine/compiler/llm-invocations/generateValidatedProse.d.ts.map +0 -1
  314. package/dist/src/engine/compiler/llm-invocations/generateValidatedProse.js.map +0 -1
  315. package/dist/src/engine/compiler/schemas/index.d.ts.map +0 -1
  316. package/dist/src/engine/compiler/schemas/index.js.map +0 -1
  317. package/dist/src/engine/compiler/schemas/milestones.d.ts.map +0 -1
  318. package/dist/src/engine/compiler/schemas/milestones.js.map +0 -1
  319. package/dist/src/engine/compiler/seed-ingestion.d.ts.map +0 -1
  320. package/dist/src/engine/compiler/seed-ingestion.js.map +0 -1
  321. package/dist/src/engine/compiler/telemetry.d.ts.map +0 -1
  322. package/dist/src/engine/compiler/telemetry.js.map +0 -1
  323. package/dist/src/engine/compiler/types.d.ts.map +0 -1
  324. package/dist/src/engine/compiler/types.js.map +0 -1
  325. package/dist/src/engine/compiler/validate.d.ts.map +0 -1
  326. package/dist/src/engine/compiler/validate.js.map +0 -1
  327. package/dist/src/engine/core/agent-generator.d.ts.map +0 -1
  328. package/dist/src/engine/core/agent-generator.js.map +0 -1
  329. package/dist/src/engine/core/kernel.d.ts.map +0 -1
  330. package/dist/src/engine/core/kernel.js.map +0 -1
  331. package/dist/src/engine/core/progression.d.ts.map +0 -1
  332. package/dist/src/engine/core/progression.js.map +0 -1
  333. package/dist/src/engine/core/rng.d.ts.map +0 -1
  334. package/dist/src/engine/core/rng.js.map +0 -1
  335. package/dist/src/engine/core/state.d.ts.map +0 -1
  336. package/dist/src/engine/core/state.js.map +0 -1
  337. package/dist/src/engine/effect-registry.d.ts.map +0 -1
  338. package/dist/src/engine/effect-registry.js.map +0 -1
  339. package/dist/src/engine/event-taxonomy.d.ts.map +0 -1
  340. package/dist/src/engine/event-taxonomy.js.map +0 -1
  341. package/dist/src/engine/index.d.ts.map +0 -1
  342. package/dist/src/engine/index.js.map +0 -1
  343. package/dist/src/engine/lunar/effects.d.ts.map +0 -1
  344. package/dist/src/engine/lunar/effects.js.map +0 -1
  345. package/dist/src/engine/lunar/events.d.ts.map +0 -1
  346. package/dist/src/engine/lunar/events.js.map +0 -1
  347. package/dist/src/engine/lunar/fingerprint.d.ts.map +0 -1
  348. package/dist/src/engine/lunar/fingerprint.js.map +0 -1
  349. package/dist/src/engine/lunar/hooks.d.ts.map +0 -1
  350. package/dist/src/engine/lunar/hooks.js.map +0 -1
  351. package/dist/src/engine/lunar/index.d.ts.map +0 -1
  352. package/dist/src/engine/lunar/index.js.map +0 -1
  353. package/dist/src/engine/lunar/metrics.d.ts.map +0 -1
  354. package/dist/src/engine/lunar/metrics.js.map +0 -1
  355. package/dist/src/engine/lunar/milestones.d.ts.map +0 -1
  356. package/dist/src/engine/lunar/milestones.js.map +0 -1
  357. package/dist/src/engine/lunar/names.d.ts.map +0 -1
  358. package/dist/src/engine/lunar/names.js.map +0 -1
  359. package/dist/src/engine/lunar/politics.d.ts.map +0 -1
  360. package/dist/src/engine/lunar/politics.js.map +0 -1
  361. package/dist/src/engine/lunar/presets.d.ts.map +0 -1
  362. package/dist/src/engine/lunar/presets.js.map +0 -1
  363. package/dist/src/engine/lunar/progression-hooks.d.ts.map +0 -1
  364. package/dist/src/engine/lunar/progression-hooks.js.map +0 -1
  365. package/dist/src/engine/lunar/prompts.d.ts.map +0 -1
  366. package/dist/src/engine/lunar/prompts.js.map +0 -1
  367. package/dist/src/engine/lunar/reactions.d.ts.map +0 -1
  368. package/dist/src/engine/lunar/reactions.js.map +0 -1
  369. package/dist/src/engine/lunar/research-bundle.d.ts.map +0 -1
  370. package/dist/src/engine/lunar/research-bundle.js.map +0 -1
  371. package/dist/src/engine/mars/effects.d.ts.map +0 -1
  372. package/dist/src/engine/mars/effects.js.map +0 -1
  373. package/dist/src/engine/mars/events.d.ts.map +0 -1
  374. package/dist/src/engine/mars/events.js.map +0 -1
  375. package/dist/src/engine/mars/fingerprint.d.ts.map +0 -1
  376. package/dist/src/engine/mars/fingerprint.js.map +0 -1
  377. package/dist/src/engine/mars/hooks.d.ts.map +0 -1
  378. package/dist/src/engine/mars/hooks.js.map +0 -1
  379. package/dist/src/engine/mars/index.d.ts.map +0 -1
  380. package/dist/src/engine/mars/index.js.map +0 -1
  381. package/dist/src/engine/mars/metrics.d.ts.map +0 -1
  382. package/dist/src/engine/mars/metrics.js.map +0 -1
  383. package/dist/src/engine/mars/milestones.d.ts.map +0 -1
  384. package/dist/src/engine/mars/milestones.js.map +0 -1
  385. package/dist/src/engine/mars/names.d.ts.map +0 -1
  386. package/dist/src/engine/mars/names.js.map +0 -1
  387. package/dist/src/engine/mars/politics.d.ts.map +0 -1
  388. package/dist/src/engine/mars/politics.js.map +0 -1
  389. package/dist/src/engine/mars/presets.d.ts.map +0 -1
  390. package/dist/src/engine/mars/presets.js.map +0 -1
  391. package/dist/src/engine/mars/progression-hooks.d.ts.map +0 -1
  392. package/dist/src/engine/mars/progression-hooks.js.map +0 -1
  393. package/dist/src/engine/mars/prompts.d.ts.map +0 -1
  394. package/dist/src/engine/mars/prompts.js.map +0 -1
  395. package/dist/src/engine/mars/reactions.d.ts.map +0 -1
  396. package/dist/src/engine/mars/reactions.js.map +0 -1
  397. package/dist/src/engine/mars/research-bundle.d.ts.map +0 -1
  398. package/dist/src/engine/mars/research-bundle.js.map +0 -1
  399. package/dist/src/engine/metric-registry.d.ts.map +0 -1
  400. package/dist/src/engine/metric-registry.js.map +0 -1
  401. package/dist/src/engine/provider-resolver.d.ts.map +0 -1
  402. package/dist/src/engine/provider-resolver.js.map +0 -1
  403. package/dist/src/engine/types.d.ts.map +0 -1
  404. package/dist/src/runtime/agent-memory.d.ts.map +0 -1
  405. package/dist/src/runtime/agent-memory.js.map +0 -1
  406. package/dist/src/runtime/agent-reactions.d.ts.map +0 -1
  407. package/dist/src/runtime/agent-reactions.js.map +0 -1
  408. package/dist/src/runtime/batch.d.ts.map +0 -1
  409. package/dist/src/runtime/batch.js.map +0 -1
  410. package/dist/src/runtime/chat-agents.d.ts.map +0 -1
  411. package/dist/src/runtime/chat-agents.js.map +0 -1
  412. package/dist/src/runtime/citations-catalog.d.ts.map +0 -1
  413. package/dist/src/runtime/citations-catalog.js.map +0 -1
  414. package/dist/src/runtime/commander-setup.d.ts.map +0 -1
  415. package/dist/src/runtime/commander-setup.js.map +0 -1
  416. package/dist/src/runtime/contracts.d.ts.map +0 -1
  417. package/dist/src/runtime/cost-tracker.d.ts.map +0 -1
  418. package/dist/src/runtime/cost-tracker.js.map +0 -1
  419. package/dist/src/runtime/departments.d.ts.map +0 -1
  420. package/dist/src/runtime/departments.js.map +0 -1
  421. package/dist/src/runtime/director.d.ts.map +0 -1
  422. package/dist/src/runtime/director.js.map +0 -1
  423. package/dist/src/runtime/economics-profile.d.ts.map +0 -1
  424. package/dist/src/runtime/economics-profile.js.map +0 -1
  425. package/dist/src/runtime/emergent-setup.d.ts.map +0 -1
  426. package/dist/src/runtime/emergent-setup.js.map +0 -1
  427. package/dist/src/runtime/generic-fingerprint.d.ts.map +0 -1
  428. package/dist/src/runtime/generic-fingerprint.js.map +0 -1
  429. package/dist/src/runtime/hexaco-cues/trajectory.d.ts.map +0 -1
  430. package/dist/src/runtime/hexaco-cues/trajectory.js.map +0 -1
  431. package/dist/src/runtime/hexaco-cues/translation.d.ts.map +0 -1
  432. package/dist/src/runtime/hexaco-cues/translation.js.map +0 -1
  433. package/dist/src/runtime/index.d.ts.map +0 -1
  434. package/dist/src/runtime/index.js.map +0 -1
  435. package/dist/src/runtime/llm-invocations/generateValidatedObject.d.ts.map +0 -1
  436. package/dist/src/runtime/llm-invocations/generateValidatedObject.js.map +0 -1
  437. package/dist/src/runtime/llm-invocations/sendAndValidate.d.ts.map +0 -1
  438. package/dist/src/runtime/llm-invocations/sendAndValidate.js.map +0 -1
  439. package/dist/src/runtime/orchestrator.d.ts.map +0 -1
  440. package/dist/src/runtime/orchestrator.js.map +0 -1
  441. package/dist/src/runtime/output-writer.d.ts.map +0 -1
  442. package/dist/src/runtime/output-writer.js.map +0 -1
  443. package/dist/src/runtime/parsers.d.ts.map +0 -1
  444. package/dist/src/runtime/parsers.js.map +0 -1
  445. package/dist/src/runtime/pricing.d.ts.map +0 -1
  446. package/dist/src/runtime/pricing.js.map +0 -1
  447. package/dist/src/runtime/provider-errors.d.ts.map +0 -1
  448. package/dist/src/runtime/provider-errors.js.map +0 -1
  449. package/dist/src/runtime/reaction-step.d.ts.map +0 -1
  450. package/dist/src/runtime/reaction-step.js.map +0 -1
  451. package/dist/src/runtime/research/knowledge-base.d.ts.map +0 -1
  452. package/dist/src/runtime/research/knowledge-base.js.map +0 -1
  453. package/dist/src/runtime/research/research-memory.d.ts.map +0 -1
  454. package/dist/src/runtime/research/research-memory.js.map +0 -1
  455. package/dist/src/runtime/research/research.d.ts.map +0 -1
  456. package/dist/src/runtime/research/research.js.map +0 -1
  457. package/dist/src/runtime/research/scenario-research.d.ts.map +0 -1
  458. package/dist/src/runtime/research/scenario-research.js.map +0 -1
  459. package/dist/src/runtime/research/scenarios.d.ts.map +0 -1
  460. package/dist/src/runtime/research/scenarios.js.map +0 -1
  461. package/dist/src/runtime/runtime-helpers.d.ts.map +0 -1
  462. package/dist/src/runtime/runtime-helpers.js.map +0 -1
  463. package/dist/src/runtime/schemas/commander.d.ts.map +0 -1
  464. package/dist/src/runtime/schemas/commander.js.map +0 -1
  465. package/dist/src/runtime/schemas/department.d.ts.map +0 -1
  466. package/dist/src/runtime/schemas/department.js.map +0 -1
  467. package/dist/src/runtime/schemas/director.d.ts.map +0 -1
  468. package/dist/src/runtime/schemas/director.js.map +0 -1
  469. package/dist/src/runtime/schemas/index.d.ts.map +0 -1
  470. package/dist/src/runtime/schemas/index.js.map +0 -1
  471. package/dist/src/runtime/schemas/reactions.d.ts.map +0 -1
  472. package/dist/src/runtime/schemas/reactions.js.map +0 -1
  473. package/dist/src/runtime/schemas/verdict.d.ts.map +0 -1
  474. package/dist/src/runtime/schemas/verdict.js.map +0 -1
  475. package/dist/src/runtime/tool-ledger.d.ts.map +0 -1
  476. package/dist/src/runtime/tool-ledger.js.map +0 -1
  477. /package/dist/{src/cli → cli}/sim-config.d.ts +0 -0
  478. /package/dist/{src/cli → cli}/sim-config.js +0 -0
  479. /package/dist/{src/cli → cli}/types.d.ts +0 -0
  480. /package/dist/{src/cli → cli}/types.js +0 -0
  481. /package/dist/{src/engine → engine}/_template/index.d.ts +0 -0
  482. /package/dist/{src/engine → engine}/_template/index.js +0 -0
  483. /package/dist/{src/engine → engine}/compiler/cache.d.ts +0 -0
  484. /package/dist/{src/engine → engine}/compiler/cache.js +0 -0
  485. /package/dist/{src/engine → engine}/compiler/generate-director.d.ts +0 -0
  486. /package/dist/{src/engine → engine}/compiler/generate-director.js +0 -0
  487. /package/dist/{src/engine → engine}/compiler/generate-fingerprint.d.ts +0 -0
  488. /package/dist/{src/engine → engine}/compiler/generate-fingerprint.js +0 -0
  489. /package/dist/{src/engine → engine}/compiler/generate-milestones.d.ts +0 -0
  490. /package/dist/{src/engine → engine}/compiler/generate-milestones.js +0 -0
  491. /package/dist/{src/engine → engine}/compiler/generate-politics.d.ts +0 -0
  492. /package/dist/{src/engine → engine}/compiler/generate-politics.js +0 -0
  493. /package/dist/{src/engine → engine}/compiler/generate-progression.d.ts +0 -0
  494. /package/dist/{src/engine → engine}/compiler/generate-progression.js +0 -0
  495. /package/dist/{src/engine → engine}/compiler/generate-prompts.d.ts +0 -0
  496. /package/dist/{src/engine → engine}/compiler/generate-prompts.js +0 -0
  497. /package/dist/{src/engine → engine}/compiler/generate-reactions.d.ts +0 -0
  498. /package/dist/{src/engine → engine}/compiler/generate-reactions.js +0 -0
  499. /package/dist/{src/engine → engine}/compiler/index.d.ts +0 -0
  500. /package/dist/{src/engine → engine}/compiler/index.js +0 -0
  501. /package/dist/{src/engine → engine}/compiler/llm-invocations/generateValidatedCode.d.ts +0 -0
  502. /package/dist/{src/engine → engine}/compiler/llm-invocations/generateValidatedCode.js +0 -0
  503. /package/dist/{src/engine → engine}/compiler/llm-invocations/generateValidatedJson.d.ts +0 -0
  504. /package/dist/{src/engine → engine}/compiler/llm-invocations/generateValidatedJson.js +0 -0
  505. /package/dist/{src/engine → engine}/compiler/llm-invocations/generateValidatedObject.d.ts +0 -0
  506. /package/dist/{src/engine → engine}/compiler/llm-invocations/generateValidatedObject.js +0 -0
  507. /package/dist/{src/engine → engine}/compiler/llm-invocations/generateValidatedProse.d.ts +0 -0
  508. /package/dist/{src/engine → engine}/compiler/llm-invocations/generateValidatedProse.js +0 -0
  509. /package/dist/{src/engine → engine}/compiler/schemas/index.d.ts +0 -0
  510. /package/dist/{src/engine → engine}/compiler/schemas/index.js +0 -0
  511. /package/dist/{src/engine → engine}/compiler/schemas/milestones.d.ts +0 -0
  512. /package/dist/{src/engine → engine}/compiler/schemas/milestones.js +0 -0
  513. /package/dist/{src/engine → engine}/compiler/seed-ingestion.d.ts +0 -0
  514. /package/dist/{src/engine → engine}/compiler/seed-ingestion.js +0 -0
  515. /package/dist/{src/engine → engine}/compiler/telemetry.d.ts +0 -0
  516. /package/dist/{src/engine → engine}/compiler/telemetry.js +0 -0
  517. /package/dist/{src/engine → engine}/compiler/types.d.ts +0 -0
  518. /package/dist/{src/engine → engine}/compiler/types.js +0 -0
  519. /package/dist/{src/engine → engine}/compiler/validate.d.ts +0 -0
  520. /package/dist/{src/engine → engine}/compiler/validate.js +0 -0
  521. /package/dist/{src/engine → engine}/core/agent-generator.d.ts +0 -0
  522. /package/dist/{src/engine → engine}/core/agent-generator.js +0 -0
  523. /package/dist/{src/engine → engine}/core/kernel.d.ts +0 -0
  524. /package/dist/{src/engine → engine}/core/kernel.js +0 -0
  525. /package/dist/{src/engine → engine}/core/progression.d.ts +0 -0
  526. /package/dist/{src/engine → engine}/core/progression.js +0 -0
  527. /package/dist/{src/engine → engine}/core/rng.d.ts +0 -0
  528. /package/dist/{src/engine → engine}/core/rng.js +0 -0
  529. /package/dist/{src/engine → engine}/core/state.d.ts +0 -0
  530. /package/dist/{src/engine → engine}/core/state.js +0 -0
  531. /package/dist/{src/engine → engine}/effect-registry.d.ts +0 -0
  532. /package/dist/{src/engine → engine}/effect-registry.js +0 -0
  533. /package/dist/{src/engine → engine}/event-taxonomy.d.ts +0 -0
  534. /package/dist/{src/engine → engine}/event-taxonomy.js +0 -0
  535. /package/dist/{src/engine → engine}/lunar/effects.d.ts +0 -0
  536. /package/dist/{src/engine → engine}/lunar/effects.js +0 -0
  537. /package/dist/{src/engine → engine}/lunar/events.d.ts +0 -0
  538. /package/dist/{src/engine → engine}/lunar/events.js +0 -0
  539. /package/dist/{src/engine → engine}/lunar/fingerprint.d.ts +0 -0
  540. /package/dist/{src/engine → engine}/lunar/fingerprint.js +0 -0
  541. /package/dist/{src/engine → engine}/lunar/hooks.d.ts +0 -0
  542. /package/dist/{src/engine → engine}/lunar/hooks.js +0 -0
  543. /package/dist/{src/engine → engine}/lunar/index.d.ts +0 -0
  544. /package/dist/{src/engine → engine}/lunar/index.js +0 -0
  545. /package/dist/{src/engine → engine}/lunar/metrics.d.ts +0 -0
  546. /package/dist/{src/engine → engine}/lunar/metrics.js +0 -0
  547. /package/dist/{src/engine → engine}/lunar/milestones.d.ts +0 -0
  548. /package/dist/{src/engine → engine}/lunar/milestones.js +0 -0
  549. /package/dist/{src/engine → engine}/lunar/names.d.ts +0 -0
  550. /package/dist/{src/engine → engine}/lunar/names.js +0 -0
  551. /package/dist/{src/engine → engine}/lunar/politics.d.ts +0 -0
  552. /package/dist/{src/engine → engine}/lunar/politics.js +0 -0
  553. /package/dist/{src/engine → engine}/lunar/presets.d.ts +0 -0
  554. /package/dist/{src/engine → engine}/lunar/presets.js +0 -0
  555. /package/dist/{src/engine → engine}/lunar/progression-hooks.d.ts +0 -0
  556. /package/dist/{src/engine → engine}/lunar/progression-hooks.js +0 -0
  557. /package/dist/{src/engine → engine}/lunar/prompts.d.ts +0 -0
  558. /package/dist/{src/engine → engine}/lunar/prompts.js +0 -0
  559. /package/dist/{src/engine → engine}/lunar/reactions.d.ts +0 -0
  560. /package/dist/{src/engine → engine}/lunar/reactions.js +0 -0
  561. /package/dist/{src/engine → engine}/lunar/research-bundle.d.ts +0 -0
  562. /package/dist/{src/engine → engine}/lunar/research-bundle.js +0 -0
  563. /package/dist/{src/engine → engine}/lunar/scenario.json +0 -0
  564. /package/dist/{src/engine → engine}/mars/effects.d.ts +0 -0
  565. /package/dist/{src/engine → engine}/mars/effects.js +0 -0
  566. /package/dist/{src/engine → engine}/mars/events.d.ts +0 -0
  567. /package/dist/{src/engine → engine}/mars/events.js +0 -0
  568. /package/dist/{src/engine → engine}/mars/fingerprint.d.ts +0 -0
  569. /package/dist/{src/engine → engine}/mars/fingerprint.js +0 -0
  570. /package/dist/{src/engine → engine}/mars/hooks.d.ts +0 -0
  571. /package/dist/{src/engine → engine}/mars/hooks.js +0 -0
  572. /package/dist/{src/engine → engine}/mars/index.d.ts +0 -0
  573. /package/dist/{src/engine → engine}/mars/index.js +0 -0
  574. /package/dist/{src/engine → engine}/mars/metrics.d.ts +0 -0
  575. /package/dist/{src/engine → engine}/mars/metrics.js +0 -0
  576. /package/dist/{src/engine → engine}/mars/milestones.d.ts +0 -0
  577. /package/dist/{src/engine → engine}/mars/milestones.js +0 -0
  578. /package/dist/{src/engine → engine}/mars/names.d.ts +0 -0
  579. /package/dist/{src/engine → engine}/mars/names.js +0 -0
  580. /package/dist/{src/engine → engine}/mars/politics.d.ts +0 -0
  581. /package/dist/{src/engine → engine}/mars/politics.js +0 -0
  582. /package/dist/{src/engine → engine}/mars/presets.d.ts +0 -0
  583. /package/dist/{src/engine → engine}/mars/presets.js +0 -0
  584. /package/dist/{src/engine → engine}/mars/progression-hooks.d.ts +0 -0
  585. /package/dist/{src/engine → engine}/mars/progression-hooks.js +0 -0
  586. /package/dist/{src/engine → engine}/mars/prompts.d.ts +0 -0
  587. /package/dist/{src/engine → engine}/mars/prompts.js +0 -0
  588. /package/dist/{src/engine → engine}/mars/reactions.d.ts +0 -0
  589. /package/dist/{src/engine → engine}/mars/reactions.js +0 -0
  590. /package/dist/{src/engine → engine}/mars/research-bundle.d.ts +0 -0
  591. /package/dist/{src/engine → engine}/mars/research-bundle.js +0 -0
  592. /package/dist/{src/engine → engine}/mars/scenario.json +0 -0
  593. /package/dist/{src/engine → engine}/metric-registry.d.ts +0 -0
  594. /package/dist/{src/engine → engine}/metric-registry.js +0 -0
  595. /package/dist/{src/engine → engine}/provider-resolver.d.ts +0 -0
  596. /package/dist/{src/engine → engine}/types.d.ts +0 -0
  597. /package/dist/{src/engine → engine}/types.js +0 -0
  598. /package/dist/{src/runtime → runtime}/agent-memory.d.ts +0 -0
  599. /package/dist/{src/runtime → runtime}/agent-memory.js +0 -0
  600. /package/dist/{src/runtime → runtime}/agent-reactions.d.ts +0 -0
  601. /package/dist/{src/runtime → runtime}/agent-reactions.js +0 -0
  602. /package/dist/{src/runtime → runtime}/batch.d.ts +0 -0
  603. /package/dist/{src/runtime → runtime}/batch.js +0 -0
  604. /package/dist/{src/runtime → runtime}/chat-agents.d.ts +0 -0
  605. /package/dist/{src/runtime → runtime}/chat-agents.js +0 -0
  606. /package/dist/{src/runtime → runtime}/citations-catalog.d.ts +0 -0
  607. /package/dist/{src/runtime → runtime}/citations-catalog.js +0 -0
  608. /package/dist/{src/runtime → runtime}/commander-setup.d.ts +0 -0
  609. /package/dist/{src/runtime → runtime}/commander-setup.js +0 -0
  610. /package/dist/{src/runtime → runtime}/contracts.d.ts +0 -0
  611. /package/dist/{src/runtime → runtime}/contracts.js +0 -0
  612. /package/dist/{src/runtime → runtime}/cost-tracker.d.ts +0 -0
  613. /package/dist/{src/runtime → runtime}/cost-tracker.js +0 -0
  614. /package/dist/{src/runtime → runtime}/departments.d.ts +0 -0
  615. /package/dist/{src/runtime → runtime}/departments.js +0 -0
  616. /package/dist/{src/runtime → runtime}/director.d.ts +0 -0
  617. /package/dist/{src/runtime → runtime}/director.js +0 -0
  618. /package/dist/{src/runtime → runtime}/economics-profile.d.ts +0 -0
  619. /package/dist/{src/runtime → runtime}/economics-profile.js +0 -0
  620. /package/dist/{src/runtime → runtime}/emergent-setup.d.ts +0 -0
  621. /package/dist/{src/runtime → runtime}/emergent-setup.js +0 -0
  622. /package/dist/{src/runtime → runtime}/generic-fingerprint.d.ts +0 -0
  623. /package/dist/{src/runtime → runtime}/generic-fingerprint.js +0 -0
  624. /package/dist/{src/runtime → runtime}/hexaco-cues/trajectory.d.ts +0 -0
  625. /package/dist/{src/runtime → runtime}/hexaco-cues/trajectory.js +0 -0
  626. /package/dist/{src/runtime → runtime}/hexaco-cues/translation.d.ts +0 -0
  627. /package/dist/{src/runtime → runtime}/hexaco-cues/translation.js +0 -0
  628. /package/dist/{src/runtime → runtime}/index.d.ts +0 -0
  629. /package/dist/{src/runtime → runtime}/index.js +0 -0
  630. /package/dist/{src/runtime → runtime}/llm-invocations/generateValidatedObject.d.ts +0 -0
  631. /package/dist/{src/runtime → runtime}/llm-invocations/generateValidatedObject.js +0 -0
  632. /package/dist/{src/runtime → runtime}/llm-invocations/sendAndValidate.d.ts +0 -0
  633. /package/dist/{src/runtime → runtime}/llm-invocations/sendAndValidate.js +0 -0
  634. /package/dist/{src/runtime → runtime}/orchestrator.d.ts +0 -0
  635. /package/dist/{src/runtime → runtime}/orchestrator.js +0 -0
  636. /package/dist/{src/runtime → runtime}/output-writer.d.ts +0 -0
  637. /package/dist/{src/runtime → runtime}/output-writer.js +0 -0
  638. /package/dist/{src/runtime → runtime}/parsers.d.ts +0 -0
  639. /package/dist/{src/runtime → runtime}/parsers.js +0 -0
  640. /package/dist/{src/runtime → runtime}/pricing.d.ts +0 -0
  641. /package/dist/{src/runtime → runtime}/pricing.js +0 -0
  642. /package/dist/{src/runtime → runtime}/provider-errors.d.ts +0 -0
  643. /package/dist/{src/runtime → runtime}/provider-errors.js +0 -0
  644. /package/dist/{src/runtime → runtime}/reaction-step.d.ts +0 -0
  645. /package/dist/{src/runtime → runtime}/reaction-step.js +0 -0
  646. /package/dist/{src/runtime → runtime}/research/knowledge-base.d.ts +0 -0
  647. /package/dist/{src/runtime → runtime}/research/knowledge-base.js +0 -0
  648. /package/dist/{src/runtime → runtime}/research/research-memory.d.ts +0 -0
  649. /package/dist/{src/runtime → runtime}/research/research-memory.js +0 -0
  650. /package/dist/{src/runtime → runtime}/research/research.d.ts +0 -0
  651. /package/dist/{src/runtime → runtime}/research/research.js +0 -0
  652. /package/dist/{src/runtime → runtime}/research/scenario-research.d.ts +0 -0
  653. /package/dist/{src/runtime → runtime}/research/scenario-research.js +0 -0
  654. /package/dist/{src/runtime → runtime}/research/scenarios.d.ts +0 -0
  655. /package/dist/{src/runtime → runtime}/research/scenarios.js +0 -0
  656. /package/dist/{src/runtime → runtime}/runtime-helpers.d.ts +0 -0
  657. /package/dist/{src/runtime → runtime}/runtime-helpers.js +0 -0
  658. /package/dist/{src/runtime → runtime}/schemas/commander.d.ts +0 -0
  659. /package/dist/{src/runtime → runtime}/schemas/commander.js +0 -0
  660. /package/dist/{src/runtime → runtime}/schemas/department.d.ts +0 -0
  661. /package/dist/{src/runtime → runtime}/schemas/department.js +0 -0
  662. /package/dist/{src/runtime → runtime}/schemas/director.d.ts +0 -0
  663. /package/dist/{src/runtime → runtime}/schemas/director.js +0 -0
  664. /package/dist/{src/runtime → runtime}/schemas/index.d.ts +0 -0
  665. /package/dist/{src/runtime → runtime}/schemas/index.js +0 -0
  666. /package/dist/{src/runtime → runtime}/schemas/reactions.d.ts +0 -0
  667. /package/dist/{src/runtime → runtime}/schemas/reactions.js +0 -0
  668. /package/dist/{src/runtime → runtime}/schemas/verdict.d.ts +0 -0
  669. /package/dist/{src/runtime → runtime}/schemas/verdict.js +0 -0
  670. /package/dist/{src/runtime → runtime}/tool-ledger.d.ts +0 -0
  671. /package/dist/{src/runtime → runtime}/tool-ledger.js +0 -0
@@ -0,0 +1,1846 @@
1
+ import { createServer } from 'node:http';
2
+ import { readFileSync, writeFileSync, existsSync, readdirSync, statSync, unlinkSync } from 'node:fs';
3
+ import { resolve, dirname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { normalizeSimulationConfig, applyDemoCaps } from './sim-config.js';
6
+ import { runPairSimulations } from './pair-runner.js';
7
+ import { marsScenario } from '../engine/mars/index.js';
8
+ import { lunarScenario } from '../engine/lunar/index.js';
9
+ import { describeCustomScenarioSource, isRunnableScenarioPackage, loadDiskCustomScenarios, } from './custom-scenarios.js';
10
+ import { IpRateLimiter } from './rate-limiter.js';
11
+ import { aggregateSchemaRetries, aggregateForgeStats, aggregateCacheStats, aggregateProviderErrors, } from './retry-stats.js';
12
+ import { createCompilerTelemetry } from '../engine/compiler/telemetry.js';
13
+ import { openSessionStore } from './session-store.js';
14
+ import { generateSessionTitle } from './session-title.js';
15
+ import { resolveServerMode } from './server/server-mode.js';
16
+ import { createRunRecord, hashLeaderConfig } from './server/run-record.js';
17
+ import { createNoopRunHistoryStore } from './server/run-history-store.js';
18
+ import { handlePublicDemoRoute } from './server/routes/public-demo.js';
19
+ import { handlePlatformApiRoute } from './server/routes/platform-api.js';
20
+ function projectScenarioForClient(sc) {
21
+ return {
22
+ id: sc.id,
23
+ version: sc.version,
24
+ labels: sc.labels,
25
+ theme: sc.theme,
26
+ setup: sc.setup,
27
+ departments: sc.departments.map(d => ({ id: d.id, label: d.label, role: d.role, icon: d.icon })),
28
+ presets: sc.presets,
29
+ ui: sc.ui,
30
+ policies: {
31
+ // Compiled scenarios can express policies either as
32
+ // { toolForging: true } (boolean shorthand) or as
33
+ // { toolForging: { enabled: true } } (object with flags).
34
+ // The server crashed with "Cannot read properties of
35
+ // undefined (reading 'enabled')" on the shorthand form.
36
+ // Defensive reader handles both shapes and missing entries.
37
+ toolForging: typeof sc.policies?.toolForging === 'object'
38
+ ? Boolean(sc.policies.toolForging.enabled)
39
+ : Boolean(sc.policies?.toolForging),
40
+ bulletin: typeof sc.policies?.bulletin === 'object'
41
+ ? Boolean(sc.policies.bulletin.enabled)
42
+ : Boolean(sc.policies?.bulletin),
43
+ characterChat: typeof sc.policies?.characterChat === 'object'
44
+ ? Boolean(sc.policies.characterChat.enabled)
45
+ : Boolean(sc.policies?.characterChat),
46
+ },
47
+ };
48
+ }
49
+ const __dirname = dirname(fileURLToPath(import.meta.url));
50
+ /** Resolved paracosm version from package.json (for docs header and API responses). */
51
+ const PARACOSM_VERSION = (() => {
52
+ try {
53
+ const pkgPath = resolve(__dirname, '..', '..', 'package.json');
54
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
55
+ return pkg.version || 'unknown';
56
+ }
57
+ catch {
58
+ return 'unknown';
59
+ }
60
+ })();
61
+ function readBody(req) {
62
+ return new Promise((resolveBody, reject) => {
63
+ let body = '';
64
+ req.on('data', chunk => { body += chunk; });
65
+ req.on('end', () => resolveBody(body));
66
+ req.on('error', reject);
67
+ });
68
+ }
69
+ export function createMarsServer(options = {}) {
70
+ const env = options.env ?? process.env;
71
+ const serverMode = resolveServerMode(env);
72
+ // Rate limit default: 1 simulation per IP per day for the public-demo
73
+ // path. Even on DEMO_MODELS + DEMO_EXECUTION a run costs ~$0.40 against
74
+ // the host's keys, so 1/day caps worst-case monthly spend at roughly
75
+ // $30 × unique-daily-IPs. Users who want more runs provide their own
76
+ // key, which fully bypasses rate limiting. Override with RATE_LIMIT
77
+ // env var or maxSimsPerDay option when hosting on your own infra.
78
+ const maxSims = options.maxSimsPerDay ?? parseInt(env.RATE_LIMIT || '1', 10);
79
+ const adminWrite = (env.ADMIN_WRITE || 'false').toLowerCase() === 'true';
80
+ const scenarioDir = options.scenarioDir ?? resolve(__dirname, '..', '..', 'scenarios');
81
+ // Rate-limit state survives pm2 restarts via a JSON file alongside
82
+ // the repo. Without this, a restart gives every blocked IP a full
83
+ // fresh quota. APP_DIR is the install location on the Linode
84
+ // (/opt/paracosm); dev runs default to `.` so the cache file lands
85
+ // next to the project root.
86
+ const rateLimitStatePath = resolve(env.APP_DIR || '.', '.rate-limit.json');
87
+ const rateLimiter = maxSims > 0
88
+ ? new IpRateLimiter(maxSims, 5, 200, rateLimitStatePath)
89
+ : null;
90
+ // Output retention: sweep simulation output JSON older than
91
+ // OUTPUT_RETENTION_DAYS on boot. /opt/paracosm/output/ otherwise
92
+ // grows unbounded (~300KB per run × N daily runs = GB over months).
93
+ // Default 30 days. Set to 0 to disable the sweep. Non-fatal on any
94
+ // filesystem error — missing dir is fine, permission denied is
95
+ // logged once and skipped.
96
+ (() => {
97
+ const retentionDays = parseInt(env.OUTPUT_RETENTION_DAYS || '30', 10);
98
+ if (retentionDays <= 0)
99
+ return;
100
+ const outputDir = resolve(env.APP_DIR || resolve(__dirname, '..', '..'), 'output');
101
+ if (!existsSync(outputDir))
102
+ return;
103
+ const cutoffMs = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
104
+ let removed = 0;
105
+ try {
106
+ for (const name of readdirSync(outputDir)) {
107
+ if (!name.endsWith('.json'))
108
+ continue;
109
+ const full = resolve(outputDir, name);
110
+ try {
111
+ const stat = statSync(full);
112
+ if (stat.mtimeMs < cutoffMs) {
113
+ unlinkSync(full);
114
+ removed++;
115
+ }
116
+ }
117
+ catch { /* skip unreadable entry */ }
118
+ }
119
+ if (removed > 0) {
120
+ console.log(` [retention] Pruned ${removed} sim output files older than ${retentionDays} days from ${outputDir}`);
121
+ }
122
+ }
123
+ catch (err) {
124
+ console.log(` [retention] Sweep failed: ${err}`);
125
+ }
126
+ })();
127
+ let simConfig = null;
128
+ let simRunning = false;
129
+ let activeScenario = marsScenario;
130
+ // Raw custom scenario JSON payloads authored during this session.
131
+ const memoryScenarios = new Map();
132
+ // Runnable scenarios that can appear in the catalog and be switched to.
133
+ // Disk-loaded + builtins register into the SAME map so the switch /
134
+ // list / active-derivation code is universal — no hardcoded branches
135
+ // for specific IDs. New builtins ship by adding one more
136
+ // customScenarioCatalog.set(id, { scenario, source: 'builtin' }) here.
137
+ const customScenarioCatalog = loadDiskCustomScenarios(scenarioDir);
138
+ customScenarioCatalog.set(marsScenario.id, { scenario: marsScenario, source: 'builtin' });
139
+ customScenarioCatalog.set(lunarScenario.id, { scenario: lunarScenario, source: 'builtin' });
140
+ const clients = new Set();
141
+ // Event buffer: stores all broadcast events so new clients can catch up.
142
+ // Persisted to disk so a server restart (CI/CD redeploy, pm2 reload,
143
+ // crash) does not evaporate a completed run from the /chat and /results
144
+ // endpoints, which otherwise would tell users "no simulation data" the
145
+ // moment they navigate away and come back after a deploy.
146
+ const eventBufferPath = resolve(env.APP_DIR || '.', '.event-buffer.json');
147
+ const eventBuffer = (() => {
148
+ try {
149
+ if (existsSync(eventBufferPath)) {
150
+ const raw = readFileSync(eventBufferPath, 'utf-8');
151
+ const parsed = JSON.parse(raw);
152
+ if (Array.isArray(parsed)) {
153
+ console.log(` [event-buffer] Rehydrated ${parsed.length} buffered events from ${eventBufferPath}`);
154
+ return parsed.filter((x) => typeof x === 'string');
155
+ }
156
+ }
157
+ }
158
+ catch (err) {
159
+ console.log(` [event-buffer] Failed to rehydrate (${err}); starting empty`);
160
+ }
161
+ return [];
162
+ })();
163
+ // Parallel array of broadcast wall-clock timestamps. Index-aligned
164
+ // with eventBuffer so /admin/sessions/save can capture per-event
165
+ // pacing for replay. Rehydrated runs (post-deploy) start with no
166
+ // historical timestamps — replay of those would fall back to a
167
+ // fixed inter-event interval. Live runs after the deploy get
168
+ // accurate pacing.
169
+ const eventTimestamps = new Array(eventBuffer.length).fill(0);
170
+ // Run-state flags for auto-save on clean completion. Reset inside
171
+ // clearEventBuffer() so the next run starts fresh. See
172
+ // docs/superpowers/specs/2026-04-18-load-menu-cached-runs-design.md.
173
+ //
174
+ // AUTO_SAVE_MIN_TURNS floors the run length at one completed turn:
175
+ // accidental clicks never get saved (no turn_done → nothing to replay
176
+ // anyway), but a legitimate 1- or 2-turn run does. The earlier value
177
+ // of 3 silently excluded most hosted-demo runs from the cache ring,
178
+ // which kept the LoadMenu perpetually empty for visitors.
179
+ let currentRunAborted = false;
180
+ let currentRunSaved = false;
181
+ const AUTO_SAVE_MIN_TURNS = 1;
182
+ // Persistent storage for completed sim runs. Lives at
183
+ // `${APP_DIR}/data/sessions.db`; the directory is created on first
184
+ // open. Cap of 10 saved sessions; oldest evicts when an admin saves
185
+ // an 11th. The store is opened once at server start so the SQLite
186
+ // handle and prepared statements stay warm across requests.
187
+ let sessionStore = options.sessionStore ?? null;
188
+ const sessionsDbPath = resolve(env.APP_DIR || '.', 'data', 'sessions.db');
189
+ if (!sessionStore)
190
+ try {
191
+ sessionStore = openSessionStore(sessionsDbPath);
192
+ console.log(` [sessions] Opened session store at ${sessionsDbPath} (${sessionStore.count()} stored)`);
193
+ }
194
+ catch (err) {
195
+ // Don't crash the server if SQLite init fails (missing native binary,
196
+ // disk full, etc) — sims still run, the /sessions and /admin/sessions
197
+ // routes just return 503.
198
+ console.log(` [sessions] Failed to open session store: ${err}`);
199
+ }
200
+ const runHistoryStore = options.runHistoryStore ?? createNoopRunHistoryStore();
201
+ // Coalesce disk writes so a burst of broadcasts (e.g. 50 forge_attempt
202
+ // events during a turn) only triggers one persist call. 500ms debounce
203
+ // is short enough that a crash loses at most a half-second of events
204
+ // but long enough to avoid thrashing the disk during active runs.
205
+ let persistTimer = null;
206
+ const persistBufferSoon = () => {
207
+ if (persistTimer)
208
+ return;
209
+ persistTimer = setTimeout(() => {
210
+ persistTimer = null;
211
+ try {
212
+ writeFileSync(eventBufferPath, JSON.stringify(eventBuffer));
213
+ }
214
+ catch (err) {
215
+ console.log(` [event-buffer] Persist failed: ${err}`);
216
+ }
217
+ }, 500);
218
+ };
219
+ // ── Cross-run schema-retry ring buffer ─────────────────────────────
220
+ //
221
+ // Each completed simulation contributes its `cost.schemaRetries`
222
+ // payload to a rotating ring of the last N runs. The `/retry-stats`
223
+ // endpoint aggregates the ring so operators can answer "is 0.1.228 on
224
+ // Anthropic retrying too much on CommanderDecision?" without replaying
225
+ // individual runs. Persisted to disk so a restart doesn't wipe the
226
+ // telemetry. 100 entries ≈ 2-3 weeks of typical demo traffic.
227
+ const RETRY_RING_MAX = 100;
228
+ const retryRingPath = resolve(env.APP_DIR || '.', '.retry-stats.json');
229
+ const { schemas: retryRing, forges: forgeRing, caches: cacheRing, providerErrors: providerErrorRing, } = (() => {
230
+ try {
231
+ if (existsSync(retryRingPath)) {
232
+ const raw = readFileSync(retryRingPath, 'utf-8');
233
+ const parsed = JSON.parse(raw);
234
+ if (Array.isArray(parsed)) {
235
+ return { version: 4, schemas: parsed.slice(-RETRY_RING_MAX), forges: [], caches: [], providerErrors: [] };
236
+ }
237
+ if (parsed && typeof parsed === 'object' && Array.isArray(parsed.schemas)) {
238
+ return {
239
+ version: 4,
240
+ schemas: parsed.schemas.slice(-RETRY_RING_MAX),
241
+ forges: Array.isArray(parsed.forges) ? parsed.forges.slice(-RETRY_RING_MAX) : [],
242
+ caches: Array.isArray(parsed.caches) ? parsed.caches.slice(-RETRY_RING_MAX) : [],
243
+ providerErrors: Array.isArray(parsed.providerErrors)
244
+ ? parsed.providerErrors.slice(-RETRY_RING_MAX)
245
+ : [],
246
+ };
247
+ }
248
+ }
249
+ }
250
+ catch { /* start empty on corrupt file */ }
251
+ return { version: 4, schemas: [], forges: [], caches: [], providerErrors: [] };
252
+ })();
253
+ const persistRetryRing = () => {
254
+ try {
255
+ const payload = {
256
+ version: 4,
257
+ schemas: retryRing,
258
+ forges: forgeRing,
259
+ caches: cacheRing,
260
+ providerErrors: providerErrorRing,
261
+ };
262
+ writeFileSync(retryRingPath, JSON.stringify(payload));
263
+ }
264
+ catch (err) {
265
+ console.log(` [retry-stats] persist failed: ${err}`);
266
+ }
267
+ };
268
+ /**
269
+ * Scan the current event buffer back-to-front for the first event
270
+ * whose `_cost` payload carries a `schemaRetries` field. That event
271
+ * has the run's terminal per-schema rollup; earlier events have
272
+ * partial counts. Push to the ring and persist.
273
+ */
274
+ const captureRetrySnapshot = () => {
275
+ let capturedSchemas = false;
276
+ let capturedForges = false;
277
+ let capturedCaches = false;
278
+ let capturedProviderErrors = false;
279
+ const allCaptured = () => capturedSchemas && capturedForges && capturedCaches && capturedProviderErrors;
280
+ for (let i = eventBuffer.length - 1; i >= 0 && !allCaptured(); i--) {
281
+ const msg = eventBuffer[i];
282
+ const dataLine = msg.split('\n').find(l => l.startsWith('data: '));
283
+ if (!dataLine)
284
+ continue;
285
+ try {
286
+ const payload = JSON.parse(dataLine.slice(6));
287
+ const cost = payload?.data?._cost;
288
+ if (!cost)
289
+ continue;
290
+ if (!capturedSchemas && cost.schemaRetries && typeof cost.schemaRetries === 'object') {
291
+ retryRing.push(cost.schemaRetries);
292
+ if (retryRing.length > RETRY_RING_MAX) {
293
+ retryRing.splice(0, retryRing.length - RETRY_RING_MAX);
294
+ }
295
+ capturedSchemas = true;
296
+ }
297
+ if (!capturedForges && cost.forgeStats && typeof cost.forgeStats === 'object') {
298
+ forgeRing.push(cost.forgeStats);
299
+ if (forgeRing.length > RETRY_RING_MAX) {
300
+ forgeRing.splice(0, forgeRing.length - RETRY_RING_MAX);
301
+ }
302
+ capturedForges = true;
303
+ }
304
+ if (!capturedCaches && cost.cacheStats && typeof cost.cacheStats === 'object') {
305
+ cacheRing.push(cost.cacheStats);
306
+ if (cacheRing.length > RETRY_RING_MAX) {
307
+ cacheRing.splice(0, cacheRing.length - RETRY_RING_MAX);
308
+ }
309
+ capturedCaches = true;
310
+ }
311
+ if (!capturedProviderErrors && cost.providerErrors && typeof cost.providerErrors === 'object') {
312
+ providerErrorRing.push(cost.providerErrors);
313
+ if (providerErrorRing.length > RETRY_RING_MAX) {
314
+ providerErrorRing.splice(0, providerErrorRing.length - RETRY_RING_MAX);
315
+ }
316
+ capturedProviderErrors = true;
317
+ }
318
+ }
319
+ catch { /* skip malformed buffer entries */ }
320
+ }
321
+ if (capturedSchemas || capturedForges || capturedCaches || capturedProviderErrors) {
322
+ persistRetryRing();
323
+ }
324
+ };
325
+ /**
326
+ * Persist the current run to the session ring when it completes
327
+ * cleanly. Called from inside broadcast() on an `event: complete`
328
+ * frame. Silent no-op when conditions aren't met. Errors are logged
329
+ * but never propagate: a cache write failure must not fail the
330
+ * client-facing broadcast.
331
+ */
332
+ const autoSaveOnComplete = () => {
333
+ // Every branch logs a single [sessions] line so production can see
334
+ // in server stderr/stdout WHY a run did or did not make it into the
335
+ // ring. Without these, a save silently failing on a writable-but-
336
+ // locked SQLite file (container volume quirk) looked identical to
337
+ // a clean save from outside.
338
+ //
339
+ // Each skip/outcome also fires a `sim_saved` SSE event so the
340
+ // dashboard can surface "saved as <id>" / "save skipped: <reason>"
341
+ // without the user having to SSH the server to find out why the
342
+ // LOAD menu is still empty after a clean run.
343
+ const emitSaveStatus = (status, detail) => {
344
+ // Call broadcast directly so the client sees the status on the same
345
+ // stream as the rest of the run. Status events are included in the
346
+ // event buffer so a returning user (SSE reconnect + replay) still
347
+ // sees whether the prior run saved.
348
+ try {
349
+ broadcast('sim_saved', { status, ...detail });
350
+ }
351
+ catch { /* never fail the server on telemetry */ }
352
+ };
353
+ if (!sessionStore) {
354
+ console.log('[sessions] auto-save skipped: session store not initialized');
355
+ emitSaveStatus('skipped', { reason: 'store_not_initialized' });
356
+ return;
357
+ }
358
+ if (currentRunAborted) {
359
+ console.log('[sessions] auto-save skipped: run was aborted');
360
+ emitSaveStatus('skipped', { reason: 'run_aborted' });
361
+ return;
362
+ }
363
+ if (currentRunSaved) {
364
+ console.log('[sessions] auto-save skipped: already saved for this run');
365
+ return;
366
+ }
367
+ if (eventBuffer.length === 0) {
368
+ console.log('[sessions] auto-save skipped: empty event buffer');
369
+ emitSaveStatus('skipped', { reason: 'empty_buffer' });
370
+ return;
371
+ }
372
+ // Count completed turns. Two shapes are in play:
373
+ // 1. Legacy / test shape: `broadcast('turn_done', ...)` → frame
374
+ // line `event: turn_done\n...`.
375
+ // 2. Real production shape: the orchestrator wraps every engine
376
+ // event in `broadcast('sim', {type: 'turn_done', ...})` →
377
+ // frame line `event: sim\ndata: {"type":"turn_done",...}`.
378
+ // The prior check only matched shape (1), so every prod run
379
+ // silently skipped with `below_min_turns`. Match either shape.
380
+ const turnDoneCount = eventBuffer.reduce((n, msg) => {
381
+ if (msg.startsWith('event: turn_done\n'))
382
+ return n + 1;
383
+ if (msg.startsWith('event: sim\n') && msg.includes('"type":"turn_done"'))
384
+ return n + 1;
385
+ return n;
386
+ }, 0);
387
+ if (turnDoneCount < AUTO_SAVE_MIN_TURNS) {
388
+ console.log(`[sessions] auto-save skipped: turn_done count ${turnDoneCount} below AUTO_SAVE_MIN_TURNS (${AUTO_SAVE_MIN_TURNS})`);
389
+ emitSaveStatus('skipped', { reason: 'below_min_turns', turnDoneCount, minTurns: AUTO_SAVE_MIN_TURNS });
390
+ return;
391
+ }
392
+ try {
393
+ const now = Date.now();
394
+ const events = eventBuffer.map((sse, i) => ({
395
+ ts: eventTimestamps[i] || now,
396
+ sse,
397
+ }));
398
+ const result = sessionStore.saveSession(events);
399
+ currentRunSaved = true;
400
+ const storeCount = sessionStore.count();
401
+ console.log(`[sessions] auto-saved run ${result.id}: ${events.length} events, ${turnDoneCount} turns (store count: ${storeCount})`);
402
+ emitSaveStatus('saved', {
403
+ id: result.id,
404
+ evictedId: result.evictedId,
405
+ eventCount: events.length,
406
+ turnCount: turnDoneCount,
407
+ totalStored: storeCount,
408
+ });
409
+ // Title-generation pipeline: fire-and-forget nano-tier LLM call
410
+ // off the save hot path. A failed / slow title call must not
411
+ // block the broadcast pipeline, so the promise is intentionally
412
+ // unawaited. Emits a second `sim_saved` event when the title
413
+ // lands so the dashboard can patch its in-memory session list
414
+ // without a round-trip to /sessions.
415
+ const titleProvider = simConfig?.provider === 'anthropic' ? 'anthropic' : 'openai';
416
+ const store = sessionStore; // non-null inside this closure
417
+ void generateSessionTitle(events, titleProvider, runGenerateText).then((title) => {
418
+ if (!title)
419
+ return;
420
+ try {
421
+ store.updateTitle(result.id, title);
422
+ console.log(`[sessions] titled run ${result.id}: "${title}"`);
423
+ try {
424
+ broadcast('sim_saved', {
425
+ status: 'titled',
426
+ id: result.id,
427
+ title,
428
+ });
429
+ }
430
+ catch { /* broadcast failure must not crash the title pipeline */ }
431
+ }
432
+ catch (err) {
433
+ console.warn('[sessions] title update failed:', err);
434
+ }
435
+ });
436
+ }
437
+ catch (err) {
438
+ const message = err instanceof Error ? err.message : String(err);
439
+ console.warn('[sessions] auto-save failed:', err);
440
+ emitSaveStatus('failed', { error: message });
441
+ }
442
+ };
443
+ const broadcast = (event, data) => {
444
+ const msg = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
445
+ eventBuffer.push(msg);
446
+ eventTimestamps.push(Date.now());
447
+ persistBufferSoon();
448
+ for (const res of clients) {
449
+ try {
450
+ res.write(msg);
451
+ }
452
+ catch {
453
+ clients.delete(res);
454
+ }
455
+ }
456
+ if (event === 'sim_aborted') {
457
+ currentRunAborted = true;
458
+ }
459
+ // On simulation completion, snapshot the run's schema-retry payload
460
+ // to the cross-run ring buffer so /retry-stats can aggregate across
461
+ // production runs. We pull schemaRetries from the MOST RECENT cost
462
+ // payload observed in the current event buffer; the orchestrator
463
+ // emits it on every SSE event, so the last event has the complete
464
+ // picture.
465
+ if (event === 'complete') {
466
+ captureRetrySnapshot();
467
+ autoSaveOnComplete();
468
+ }
469
+ };
470
+ /**
471
+ * Clear the in-memory event buffer AND remove the persisted snapshot on
472
+ * disk. Without the disk drop, /clear (or a new /setup that resets the
473
+ * buffer) would leave the old run's events on disk; a subsequent server
474
+ * restart would rehydrate them and overwrite what the user expected to
475
+ * be a fresh state. Cancels any pending write so the empty state wins.
476
+ */
477
+ const clearEventBuffer = () => {
478
+ currentRunAborted = false;
479
+ currentRunSaved = false;
480
+ eventBuffer.length = 0;
481
+ eventTimestamps.length = 0;
482
+ if (persistTimer) {
483
+ clearTimeout(persistTimer);
484
+ persistTimer = null;
485
+ }
486
+ try {
487
+ if (existsSync(eventBufferPath))
488
+ unlinkSync(eventBufferPath);
489
+ }
490
+ catch { /* nothing to clean up */ }
491
+ };
492
+ const startSimulations = options.runPairSimulations ?? runPairSimulations;
493
+ // --- Cancel-on-disconnect watchdog ---------------------------------
494
+ //
495
+ // While a simulation is active, watch for the SSE client set going
496
+ // empty. If it stays empty for `disconnectGraceMs`, abort the run so
497
+ // the server stops burning API credits on work nobody is watching.
498
+ //
499
+ // The event buffer stays intact: a returning user reconnects, sees
500
+ // all events up to the cancellation point, and the dashboard labels
501
+ // the run "Unfinished" via the sim_aborted SSE event the orchestrator
502
+ // emits on cancel.
503
+ //
504
+ // Grace period handles the legitimate refresh / in-domain navigation
505
+ // case: EventSource disconnects briefly, then reconnects. The default
506
+ // 30_000ms (30s) covers a user clicking an internal link (e.g. the
507
+ // About tab, which redirects to `/`) and returning within half a
508
+ // minute — the previous 1500ms surfaced "Interrupted" badges whenever
509
+ // the user navigated away and back. Combined with the per-LLM-call
510
+ // abort gates in the orchestrator (runtime/orchestrator.ts), at most
511
+ // one in-flight call finishes after the watchdog trips regardless of
512
+ // how long the grace window is, so widening it has no worst-case
513
+ // wasted-spend cost.
514
+ const disconnectGraceMs = options.disconnectGraceMs ?? 30_000;
515
+ /** Current sim's AbortController, or null when no sim is running. */
516
+ let activeSimAbortController = null;
517
+ /** Timer id for the pending disconnect-watchdog fire. Null when disarmed. */
518
+ let disconnectWatchdogTimer = null;
519
+ const armDisconnectWatchdog = () => {
520
+ if (!activeSimAbortController)
521
+ return; // no sim running, nothing to cancel
522
+ if (disconnectWatchdogTimer)
523
+ return; // already armed
524
+ disconnectWatchdogTimer = setTimeout(() => {
525
+ disconnectWatchdogTimer = null;
526
+ if (!activeSimAbortController)
527
+ return;
528
+ if (clients.size > 0)
529
+ return; // somebody reconnected just in time
530
+ console.log(` [watchdog] No SSE clients for ${disconnectGraceMs}ms — aborting active simulation.`);
531
+ activeSimAbortController.abort();
532
+ }, disconnectGraceMs);
533
+ };
534
+ const disarmDisconnectWatchdog = () => {
535
+ if (disconnectWatchdogTimer) {
536
+ clearTimeout(disconnectWatchdogTimer);
537
+ disconnectWatchdogTimer = null;
538
+ }
539
+ };
540
+ const runGenerateText = options.generateText ?? (async (args) => {
541
+ const { generateText } = await import('@framers/agentos');
542
+ return generateText(args);
543
+ });
544
+ const runCompileScenario = options.compileScenario ?? (async (scenarioJson, compileOptions) => {
545
+ const { compileScenario } = await import('../engine/compiler/index.js');
546
+ return compileScenario(scenarioJson, compileOptions);
547
+ });
548
+ const corsHeaders = {
549
+ 'Access-Control-Allow-Origin': '*',
550
+ };
551
+ const server = createServer(async (req, res) => {
552
+ // CORS preflight for browser-based POST requests (compile, setup, chat, clear)
553
+ if (req.method === 'OPTIONS') {
554
+ res.writeHead(204, {
555
+ 'Access-Control-Allow-Origin': '*',
556
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
557
+ 'Access-Control-Allow-Headers': 'Content-Type',
558
+ 'Access-Control-Max-Age': '86400',
559
+ });
560
+ res.end();
561
+ return;
562
+ }
563
+ if (handlePublicDemoRoute(serverMode, req, res, corsHeaders)) {
564
+ return;
565
+ }
566
+ if (await handlePlatformApiRoute(serverMode, req, res, { runHistoryStore, corsHeaders })) {
567
+ return;
568
+ }
569
+ if (req.url === '/events') {
570
+ res.writeHead(200, {
571
+ 'Content-Type': 'text/event-stream',
572
+ 'Cache-Control': 'no-cache',
573
+ 'Connection': 'keep-alive',
574
+ 'Access-Control-Allow-Origin': '*',
575
+ });
576
+ res.write('event: connected\ndata: {}\n\n');
577
+ // Replay all buffered events so new clients catch up. The trailing
578
+ // `replay_done` marker lets the client distinguish historical-buffer
579
+ // events from truly live ones so toasts (transient per-event
580
+ // notifications) only fire for events that arrive AFTER the user
581
+ // reached the page, never for the replay of a prior run.
582
+ for (const msg of eventBuffer) {
583
+ try {
584
+ res.write(msg);
585
+ }
586
+ catch {
587
+ break;
588
+ }
589
+ }
590
+ try {
591
+ res.write('event: replay_done\ndata: {}\n\n');
592
+ }
593
+ catch { }
594
+ clients.add(res);
595
+ // Reconnection cancels any pending disconnect watchdog fire so
596
+ // the sim keeps running once the returning user is watching again.
597
+ disarmDisconnectWatchdog();
598
+ req.on('close', () => {
599
+ clients.delete(res);
600
+ if (clients.size === 0) {
601
+ // Start (or re-start) the grace-period countdown. If no
602
+ // client reconnects before it fires, the watchdog aborts
603
+ // the active sim.
604
+ armDisconnectWatchdog();
605
+ }
606
+ });
607
+ return;
608
+ }
609
+ if (req.url === '/scenario' && req.method === 'GET') {
610
+ const payload = JSON.stringify(projectScenarioForClient(activeScenario));
611
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
612
+ res.end(payload);
613
+ return;
614
+ }
615
+ // List scenarios that can be switched to. Builtins, disk-loaded,
616
+ // in-memory runnable customs, and any currently-active compiled
617
+ // scenario all live in the same catalog — this endpoint is a
618
+ // uniform iteration over it. No hardcoded IDs.
619
+ if (req.url === '/scenarios' && req.method === 'GET') {
620
+ const scenarios = [];
621
+ for (const [id, entry] of customScenarioCatalog) {
622
+ const sc = entry.scenario;
623
+ scenarios.push({
624
+ id,
625
+ name: sc.labels?.name || id,
626
+ description: describeCustomScenarioSource(entry.source),
627
+ departments: sc.departments?.length || 0,
628
+ source: entry.source,
629
+ });
630
+ }
631
+ // Active scenario might not be in the catalog yet (freshly
632
+ // compiled but we haven't written it back — belt-and-suspenders).
633
+ if (!customScenarioCatalog.has(activeScenario.id)) {
634
+ scenarios.push({
635
+ id: activeScenario.id,
636
+ name: activeScenario.labels?.name || activeScenario.id,
637
+ description: 'Custom compiled scenario',
638
+ departments: activeScenario.departments?.length || 0,
639
+ source: 'compiled',
640
+ });
641
+ }
642
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
643
+ res.end(JSON.stringify({ scenarios, active: activeScenario.id }));
644
+ return;
645
+ }
646
+ // Admin config: tells client what's enabled
647
+ if (req.url === '/admin-config' && req.method === 'GET') {
648
+ // Hosted-demo flag: when true, env-only API keys belong to the
649
+ // host (not the end user), so the dashboard treats env presence as
650
+ // "host is paying" and surfaces demo-mode UX (hidden model picker,
651
+ // rate-limit notice). Local dev leaves this unset and the picker
652
+ // becomes visible whenever any LLM key is configured.
653
+ const hostedDemo = serverMode === 'hosted_demo';
654
+ // Expose the effective demo caps so the Settings UI can show
655
+ // accurate `demo:N` lock labels without hardcoding the number
656
+ // in the client. Lets operators flip the env var + pm2 restart
657
+ // and the UI updates on the next page load without a redeploy.
658
+ const { DEMO_EXECUTION } = await import('./sim-config.js');
659
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
660
+ res.end(JSON.stringify({
661
+ adminWrite,
662
+ hostedDemo,
663
+ serverMode,
664
+ demoCaps: {
665
+ maxTurns: DEMO_EXECUTION.maxTurns,
666
+ maxPopulation: DEMO_EXECUTION.maxPopulation,
667
+ maxActiveDepartments: DEMO_EXECUTION.maxActiveDepartments,
668
+ },
669
+ memoryScenarios: [...memoryScenarios.keys()],
670
+ keys: {
671
+ openai: !!env.OPENAI_API_KEY,
672
+ anthropic: !!env.ANTHROPIC_API_KEY,
673
+ serper: !!env.SERPER_API_KEY,
674
+ firecrawl: !!env.FIRECRAWL_API_KEY,
675
+ tavily: !!env.TAVILY_API_KEY,
676
+ cohere: !!env.COHERE_API_KEY,
677
+ },
678
+ }));
679
+ return;
680
+ }
681
+ // Store a scenario in memory (always allowed) or save to disk (requires ADMIN_WRITE)
682
+ if (req.url === '/scenario/store' && req.method === 'POST') {
683
+ try {
684
+ const { scenario: scenarioJson, saveToDisk } = JSON.parse(await readBody(req));
685
+ if (!scenarioJson || !scenarioJson.id) {
686
+ res.writeHead(400, { 'Content-Type': 'application/json' });
687
+ res.end(JSON.stringify({ error: 'scenario with id required' }));
688
+ return;
689
+ }
690
+ // Store raw JSON in memory for this authoring session.
691
+ memoryScenarios.set(scenarioJson.id, scenarioJson);
692
+ const switchable = isRunnableScenarioPackage(scenarioJson);
693
+ if (switchable) {
694
+ customScenarioCatalog.set(scenarioJson.id, {
695
+ scenario: scenarioJson,
696
+ source: saveToDisk && adminWrite ? 'disk' : 'memory',
697
+ });
698
+ }
699
+ // Optionally save to disk if admin
700
+ let savedToDisk = false;
701
+ if (saveToDisk && adminWrite) {
702
+ const { writeFileSync, mkdirSync } = await import('node:fs');
703
+ mkdirSync(scenarioDir, { recursive: true });
704
+ writeFileSync(resolve(scenarioDir, `${scenarioJson.id}.json`), JSON.stringify(scenarioJson, null, 2));
705
+ savedToDisk = true;
706
+ if (switchable) {
707
+ customScenarioCatalog.set(scenarioJson.id, {
708
+ scenario: scenarioJson,
709
+ source: 'disk',
710
+ });
711
+ }
712
+ }
713
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
714
+ res.end(JSON.stringify({ stored: true, id: scenarioJson.id, savedToDisk, adminWrite, switchable }));
715
+ }
716
+ catch (err) {
717
+ res.writeHead(400, { 'Content-Type': 'application/json' });
718
+ res.end(JSON.stringify({ error: String(err) }));
719
+ }
720
+ return;
721
+ }
722
+ // Switch active scenario. Uniform catalog lookup — builtins
723
+ // (mars-genesis, lunar-outpost) are registered into the same map
724
+ // at server init, so this handler has NO hardcoded IDs. Any ID
725
+ // that resolves to a runnable entry switches; source JSONs that
726
+ // were /scenario/store'd but never compiled get a specific
727
+ // "needs compile" error instead of the misleading "Unknown".
728
+ if (req.url === '/scenario/switch' && req.method === 'POST') {
729
+ const { id } = JSON.parse(await readBody(req));
730
+ const entry = customScenarioCatalog.get(id);
731
+ if (entry) {
732
+ activeScenario = entry.scenario;
733
+ }
734
+ else if (memoryScenarios.has(id)) {
735
+ res.writeHead(400, { 'Content-Type': 'application/json' });
736
+ res.end(JSON.stringify({
737
+ error: `Scenario "${id}" is stored but not runnable — it's a source JSON (missing hooks, world, or canonical policies shape). Click Compile in the Scenario Editor to generate hooks before switching to it.`,
738
+ storedButUnrunnable: true,
739
+ id,
740
+ }));
741
+ return;
742
+ }
743
+ else {
744
+ res.writeHead(400, { 'Content-Type': 'application/json' });
745
+ res.end(JSON.stringify({ error: `Unknown scenario: ${id}. Use /compile or /scenario/store for custom scenarios.` }));
746
+ return;
747
+ }
748
+ clearEventBuffer();
749
+ simConfig = null;
750
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
751
+ res.end(JSON.stringify({ active: activeScenario.id, name: activeScenario.labels?.name }));
752
+ return;
753
+ }
754
+ // Compile a custom scenario JSON into a ScenarioPackage
755
+ if (req.url === '/compile' && req.method === 'POST') {
756
+ try {
757
+ const body = JSON.parse(await readBody(req));
758
+ const { scenario: scenarioJson, provider: requestedProvider, model: requestedModel, seedText, seedUrl, webSearch, maxSearches, apiKey, anthropicKey } = body;
759
+ if (!scenarioJson || typeof scenarioJson !== 'object') {
760
+ res.writeHead(400, { 'Content-Type': 'application/json' });
761
+ res.end(JSON.stringify({ error: 'scenario JSON object required' }));
762
+ return;
763
+ }
764
+ // Rate-limit compile against its own daily bucket. Each compile
765
+ // costs ~$0.10 against the host's API key, so even 10 uncontrolled
766
+ // hits is a real line item. Bypassed when the caller is not
767
+ // billing the host: either a session key was supplied, or the
768
+ // server is in local mode (PARACOSM_HOSTED_DEMO unset) where
769
+ // env keys belong to the operator.
770
+ const userSuppliedKey = !!(apiKey || anthropicKey);
771
+ const isHostedDemoCompile = serverMode === 'hosted_demo';
772
+ const hostBilled = !userSuppliedKey && isHostedDemoCompile;
773
+ if (rateLimiter && hostBilled) {
774
+ const ip = IpRateLimiter.getIp(req);
775
+ const { allowed, remaining, resetAt, limit } = rateLimiter.consumeCompile(ip);
776
+ if (!allowed) {
777
+ res.writeHead(429, {
778
+ 'Content-Type': 'application/json',
779
+ 'X-RateLimit-Limit': String(limit),
780
+ 'X-RateLimit-Remaining': '0',
781
+ 'X-RateLimit-Reset': String(Math.floor(resetAt / 1000)),
782
+ });
783
+ res.end(JSON.stringify({
784
+ error: `Compile rate limit exceeded. Maximum ${limit} compiles per day. Add your own API keys to bypass.`,
785
+ limit,
786
+ remaining: 0,
787
+ resetAt,
788
+ }));
789
+ return;
790
+ }
791
+ console.log(` [rate-limit] /compile ${ip}: ${remaining} remaining of ${limit}`);
792
+ }
793
+ // Apply user-supplied keys to env for this request so downstream
794
+ // LLM calls route to the user's account, not the host's. Keys are
795
+ // snapshotted + restored after the compile completes so they don't
796
+ // leak into subsequent unrelated requests. Placeholder values
797
+ // (e.g. "sk-...") are ignored so a masked display string never
798
+ // replaces a real key.
799
+ const compileEnvSnapshot = [];
800
+ const scopeCompileKey = (name, value) => {
801
+ if (typeof value !== 'string' || !value || value.includes('...'))
802
+ return;
803
+ compileEnvSnapshot.push([name, env[name]]);
804
+ env[name] = value;
805
+ };
806
+ scopeCompileKey('OPENAI_API_KEY', apiKey);
807
+ scopeCompileKey('ANTHROPIC_API_KEY', anthropicKey);
808
+ const restoreCompileEnv = () => {
809
+ for (const [name, prior] of compileEnvSnapshot) {
810
+ if (prior === undefined)
811
+ delete env[name];
812
+ else
813
+ env[name] = prior;
814
+ }
815
+ };
816
+ const provider = requestedProvider || (env.ANTHROPIC_API_KEY ? 'anthropic' : 'openai');
817
+ // Force the cheapest class only when the host is billing
818
+ // (hosted-demo mode + no session key). Local dev and BYO-key
819
+ // paths honor the requested model.
820
+ const demoCompileModel = provider === 'anthropic' ? 'claude-haiku-4-5-20251001' : 'gpt-5.4-nano';
821
+ const model = !hostBilled
822
+ ? (requestedModel || (provider === 'anthropic' ? 'claude-sonnet-4-6' : 'gpt-5.4-mini'))
823
+ : demoCompileModel;
824
+ // SSE progress stream
825
+ res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Access-Control-Allow-Origin': '*' });
826
+ res.write('event: status\ndata: {"status":"compiling"}\n\n');
827
+ // Forward every compile-hook fallback as an SSE event in real time
828
+ // so the dashboard surfaces degraded compiles instead of showing
829
+ // a silent success. The underlying aggregator still records the
830
+ // attempts + fallbacks for the /retry-stats ring buffer below.
831
+ const baseCompileTelemetry = createCompilerTelemetry();
832
+ const compileTelemetry = {
833
+ recordAttempt: (hookName, attempts, fromFallback) => baseCompileTelemetry.recordAttempt(hookName, attempts, fromFallback),
834
+ recordFallback: (hookName, details) => {
835
+ baseCompileTelemetry.recordFallback(hookName, details);
836
+ res.write(`event: compile_validation_fallback\ndata: ${JSON.stringify({
837
+ hookName,
838
+ attempts: details.attempts,
839
+ reason: details.reason,
840
+ rawTextExcerpt: (details.rawText ?? '').slice(-500),
841
+ })}\n\n`);
842
+ },
843
+ snapshot: () => baseCompileTelemetry.snapshot(),
844
+ };
845
+ let compiled;
846
+ try {
847
+ compiled = await runCompileScenario(scenarioJson, {
848
+ provider,
849
+ model,
850
+ cache: true,
851
+ seedText,
852
+ seedUrl,
853
+ webSearch: webSearch ?? true,
854
+ maxSearches,
855
+ telemetry: compileTelemetry,
856
+ onProgress(hookName, status) {
857
+ res.write(`event: progress\ndata: ${JSON.stringify({ hook: hookName, status })}\n\n`);
858
+ },
859
+ });
860
+ }
861
+ finally {
862
+ restoreCompileEnv();
863
+ }
864
+ // Update the active scenario for GET /scenario
865
+ activeScenario = compiled;
866
+ memoryScenarios.set(compiled.id, compiled);
867
+ customScenarioCatalog.set(compiled.id, { scenario: compiled, source: 'compiled' });
868
+ // Snapshot compile telemetry into the ring buffer so /retry-stats
869
+ // aggregates compile:* schemas alongside runtime schemas, and emit
870
+ // the rollup as SSE so the dashboard can render per-hook attempts
871
+ // and fallbacks immediately without polling the endpoint.
872
+ const compileSnap = compileTelemetry.snapshot();
873
+ if (Object.keys(compileSnap.schemaRetries).length > 0) {
874
+ retryRing.push(compileSnap.schemaRetries);
875
+ if (retryRing.length > RETRY_RING_MAX) {
876
+ retryRing.splice(0, retryRing.length - RETRY_RING_MAX);
877
+ }
878
+ persistRetryRing();
879
+ }
880
+ const perHookMetrics = {};
881
+ for (const [key, bucket] of Object.entries(compileSnap.schemaRetries)) {
882
+ perHookMetrics[key.replace(/^compile:/, '')] = {
883
+ attempts: bucket.attempts,
884
+ fromFallback: bucket.fallbacks > 0,
885
+ };
886
+ }
887
+ res.write(`event: compile_metrics\ndata: ${JSON.stringify({
888
+ hooks: perHookMetrics,
889
+ totalFallbacks: compileSnap.fallbacks.length,
890
+ })}\n\n`);
891
+ res.write(`event: complete\ndata: ${JSON.stringify({ id: compiled.id, version: compiled.version, departments: compiled.departments.length, hooks: Object.keys(compiled.hooks).filter(k => compiled.hooks[k]).length })}\n\n`);
892
+ res.end();
893
+ }
894
+ catch (err) {
895
+ if (!res.headersSent) {
896
+ res.writeHead(500, { 'Content-Type': 'application/json' });
897
+ res.end(JSON.stringify({ error: String(err) }));
898
+ }
899
+ else {
900
+ res.write(`event: error\ndata: ${JSON.stringify({ error: String(err) })}\n\n`);
901
+ res.end();
902
+ }
903
+ }
904
+ return;
905
+ }
906
+ if (req.url === '/clear' && req.method === 'POST') {
907
+ clearEventBuffer();
908
+ simConfig = null;
909
+ // Clear chat agent pool when simulation is cleared
910
+ import('../runtime/chat-agents.js').then(m => m.clearPool()).catch(() => { });
911
+ res.writeHead(200, { 'Content-Type': 'application/json' });
912
+ res.end(JSON.stringify({ cleared: true }));
913
+ return;
914
+ }
915
+ if (req.url === '/setup' && req.method === 'GET') {
916
+ res.writeHead(302, { Location: '/sim?tab=settings' });
917
+ res.end();
918
+ return;
919
+ }
920
+ // Rate limit status endpoint
921
+ // Post-simulation colonist chat
922
+ if (req.url === '/chat' && req.method === 'POST') {
923
+ // Hoist env-scoping helpers OUTSIDE the try so the catch can
924
+ // still call restoreChatEnv. Without this, an exception thrown
925
+ // between scopeChatKey() and the inner restore would leak the
926
+ // caller's keys into subsequent requests.
927
+ const chatEnvSnapshot = [];
928
+ const restoreChatEnv = () => {
929
+ for (const [name, prior] of chatEnvSnapshot) {
930
+ if (prior === undefined)
931
+ delete env[name];
932
+ else
933
+ env[name] = prior;
934
+ }
935
+ };
936
+ try {
937
+ const { agentId, message, apiKey, anthropicKey } = JSON.parse(await readBody(req));
938
+ if (!agentId || !message) {
939
+ res.writeHead(400, { 'Content-Type': 'application/json' });
940
+ res.end(JSON.stringify({ error: 'agentId and message required' }));
941
+ return;
942
+ }
943
+ // Apply user-supplied keys to env for this request so the
944
+ // downstream chat agent routes to the user's account when a
945
+ // BYO key is present. Placeholder masks like 'sk-...' are
946
+ // rejected so a displayed masked string never replaces a real key.
947
+ const chatUserKey = !!(apiKey || anthropicKey);
948
+ const scopeChatKey = (name, value) => {
949
+ if (typeof value !== 'string' || !value || value.includes('...'))
950
+ return;
951
+ chatEnvSnapshot.push([name, env[name]]);
952
+ env[name] = value;
953
+ };
954
+ scopeChatKey('OPENAI_API_KEY', apiKey);
955
+ scopeChatKey('ANTHROPIC_API_KEY', anthropicKey);
956
+ // Rate-limit chat per IP per hour. Runs against the host's
957
+ // key unless a session key was provided in the request body,
958
+ // in which case the caller is paying and the cap is bypassed
959
+ // (same contract as /setup and /compile). 200/hour leaves
960
+ // plenty of headroom for real host-billed users exploring
961
+ // colonist conversations.
962
+ if (rateLimiter && !chatUserKey) {
963
+ const ip = IpRateLimiter.getIp(req);
964
+ const { allowed, remaining, resetAt, limit } = rateLimiter.consumeChat(ip);
965
+ if (!allowed) {
966
+ res.writeHead(429, {
967
+ 'Content-Type': 'application/json',
968
+ 'X-RateLimit-Limit': String(limit),
969
+ 'X-RateLimit-Remaining': '0',
970
+ 'X-RateLimit-Reset': String(Math.floor(resetAt / 1000)),
971
+ });
972
+ res.end(JSON.stringify({
973
+ error: `Chat rate limit exceeded. Maximum ${limit} messages per hour. Try again later.`,
974
+ limit,
975
+ remaining: 0,
976
+ resetAt,
977
+ }));
978
+ return;
979
+ }
980
+ if (remaining < 20) {
981
+ // Warn in logs when a user is nearing the cap; helps diagnose
982
+ // legit-user complaints from actual abuse.
983
+ console.log(` [rate-limit] /chat ${ip}: ${remaining} remaining of ${limit}`);
984
+ }
985
+ }
986
+ // The chat route builds agents from the event buffer. If the
987
+ // buffer was lost (fresh boot with no persisted snapshot on disk,
988
+ // or the user hit /clear) there is nothing to build from. Phrase
989
+ // the error so users understand it is a server-side emptiness,
990
+ // not "the run you just finished is gone forever".
991
+ if (eventBuffer.length === 0) {
992
+ res.writeHead(400, { 'Content-Type': 'application/json' });
993
+ res.end(JSON.stringify({ error: 'Server has no simulation in memory right now. Run a new simulation from the Settings tab, or reload this page so your saved events restore from cache.' }));
994
+ return;
995
+ }
996
+ // Import chat agent system (lazy to avoid startup cost)
997
+ const { getOrCreateChatAgent, extractColonistMemories, extractColonistRoster } = await import('../runtime/chat-agents.js');
998
+ // Extract sim events and find colonist profile
999
+ const simEvents = eventBuffer
1000
+ .filter(msg => msg.startsWith('event: sim\n'))
1001
+ .map(msg => { try {
1002
+ return JSON.parse(msg.split('data: ')[1]);
1003
+ }
1004
+ catch {
1005
+ return null;
1006
+ } })
1007
+ .filter(Boolean);
1008
+ const agentReactions = simEvents
1009
+ .filter((e) => e.type === 'agent_reactions')
1010
+ .flatMap((e) => (e.data?.reactions || []).filter((r) => r.agentId === agentId || String(r.name || '').toLowerCase().includes(agentId.toLowerCase())));
1011
+ const colonist = agentReactions[0];
1012
+ // Build colonist profile
1013
+ const profile = {
1014
+ agentId,
1015
+ name: colonist?.name || agentId,
1016
+ age: colonist?.age,
1017
+ marsborn: colonist?.marsborn,
1018
+ role: colonist?.role,
1019
+ department: colonist?.department,
1020
+ specialization: colonist?.specialization,
1021
+ hexaco: colonist?.hexaco,
1022
+ psychScore: colonist?.psychScore,
1023
+ boneDensity: colonist?.boneDensity,
1024
+ radiation: colonist?.radiation,
1025
+ };
1026
+ // Extract simulation memories for this colonist
1027
+ const memories = extractColonistMemories(agentId, simEvents);
1028
+ // Extract the full colony roster from the latest colony_snapshot so
1029
+ // the chat agent knows who else exists. Without this, the agent
1030
+ // confabulates fake bios for any name the user invents.
1031
+ const roster = extractColonistRoster(simEvents);
1032
+ // Get or create the agent (lazy init with memory seeding)
1033
+ const provider = (simConfig?.provider || (env.ANTHROPIC_API_KEY && !env.OPENAI_API_KEY ? 'anthropic' : 'openai'));
1034
+ const { session, isNew } = await getOrCreateChatAgent(profile, memories, {
1035
+ provider,
1036
+ settlementNoun: activeScenario.labels?.settlementNoun,
1037
+ populationNoun: activeScenario.labels?.populationNoun,
1038
+ roster,
1039
+ });
1040
+ // Send message through the agent session (full history + memory + RAG automatic)
1041
+ const result = await session.send(message);
1042
+ restoreChatEnv();
1043
+ // Surface per-turn token usage + cost so the dashboard footer
1044
+ // can fold chat spend into the run-total display. Without this,
1045
+ // the cost readout only reflected the simulation pipeline and
1046
+ // users racked up chat charges invisibly.
1047
+ const usage = result.usage ?? {};
1048
+ res.writeHead(200, { ...corsHeaders, 'Content-Type': 'application/json' });
1049
+ res.end(JSON.stringify({
1050
+ reply: result.text,
1051
+ colonist: profile.name,
1052
+ memorySeeded: memories.length,
1053
+ firstMessage: isNew,
1054
+ usage: {
1055
+ totalTokens: usage.totalTokens ?? 0,
1056
+ costUSD: usage.costUSD ?? 0,
1057
+ promptTokens: usage.promptTokens ?? 0,
1058
+ completionTokens: usage.completionTokens ?? 0,
1059
+ cacheReadTokens: usage.cacheReadTokens ?? 0,
1060
+ cacheCreationTokens: usage.cacheCreationTokens ?? 0,
1061
+ },
1062
+ }));
1063
+ }
1064
+ catch (err) {
1065
+ // Restore env on error path too — mutations happened before
1066
+ // whatever threw. Without this, a failed chat call leaves the
1067
+ // caller's keys persisted in env for subsequent requests.
1068
+ try {
1069
+ restoreChatEnv();
1070
+ }
1071
+ catch { }
1072
+ res.writeHead(500, { 'Content-Type': 'application/json' });
1073
+ res.end(JSON.stringify({ error: String(err) }));
1074
+ }
1075
+ return;
1076
+ }
1077
+ // GET /results — full structured simulation results including verdict.
1078
+ // Reconstructs per-leader payloads from the SSE buffer so consumers
1079
+ // get the same rich data the dashboard sees, without having to scrape
1080
+ // raw events themselves.
1081
+ if (req.url === '/results' && req.method === 'GET') {
1082
+ const simEvents = eventBuffer
1083
+ .filter(msg => msg.startsWith('event: sim\n') || msg.startsWith('event: result\n') || msg.startsWith('event: verdict\n') || msg.startsWith('event: complete\n'))
1084
+ .map(msg => {
1085
+ const lines = msg.split('\n');
1086
+ const eventType = lines[0]?.replace('event: ', '') || '';
1087
+ try {
1088
+ return { event: eventType, data: JSON.parse(lines[1]?.replace('data: ', '') || '{}') };
1089
+ }
1090
+ catch {
1091
+ return { event: eventType, data: {} };
1092
+ }
1093
+ });
1094
+ const results = simEvents.filter(e => e.event === 'result').map(e => e.data);
1095
+ const verdict = simEvents.find(e => e.event === 'verdict')?.data || null;
1096
+ const isComplete = simEvents.some(e => e.event === 'complete');
1097
+ const turns = simEvents.filter(e => e.event === 'sim' && e.data?.type === 'turn_start').length / 2;
1098
+ // Reconstruct per-leader timelines from the sim event stream.
1099
+ // Group every sim event by leader name, then bucket interesting
1100
+ // payload types into typed lists so consumers can pull turn-by-turn
1101
+ // crisis info, dept reports, decisions, forges, citations, reactions.
1102
+ const byLeader = new Map();
1103
+ const ensureLeader = (name) => {
1104
+ if (!byLeader.has(name))
1105
+ byLeader.set(name, { events: [], decisions: [], forges: [], citations: [], deptReports: [], agentReactions: [], promotions: [], colonySnapshots: [] });
1106
+ return byLeader.get(name);
1107
+ };
1108
+ // Track decision pending state so we can attach it to outcomes per event
1109
+ const pendingDecision = new Map();
1110
+ for (const e of simEvents) {
1111
+ if (e.event !== 'sim')
1112
+ continue;
1113
+ const inner = e.data;
1114
+ const type = String(inner.type || '');
1115
+ const leader = String(inner.leader || '');
1116
+ if (!leader)
1117
+ continue;
1118
+ const slot = ensureLeader(leader);
1119
+ const data = inner.data ?? {};
1120
+ const turn = data.turn;
1121
+ const year = data.year;
1122
+ const eventIndex = data.eventIndex;
1123
+ const pendKey = `${leader}-${turn}-${eventIndex ?? 0}`;
1124
+ if (type === 'event_start') {
1125
+ slot.events.push({ turn, year, eventIndex, title: data.title, category: data.category, description: data.description, emergent: data.emergent });
1126
+ }
1127
+ else if (type === 'turn_start' && data.title && data.title !== 'Director generating...') {
1128
+ slot.events.push({ turn, year, title: data.title, category: data.category, description: data.crisis, emergent: data.emergent });
1129
+ }
1130
+ else if (type === 'commander_decided') {
1131
+ pendingDecision.set(pendKey, {
1132
+ decision: data.decision,
1133
+ rationale: data.rationale,
1134
+ selectedPolicies: data.selectedPolicies,
1135
+ });
1136
+ }
1137
+ else if (type === 'outcome') {
1138
+ const p = pendingDecision.get(pendKey);
1139
+ slot.decisions.push({ turn, year, eventIndex, ...p, outcome: data.outcome });
1140
+ pendingDecision.delete(pendKey);
1141
+ }
1142
+ else if (type === 'dept_done') {
1143
+ const dept = data.department;
1144
+ const cites = data.citationList || [];
1145
+ slot.deptReports.push({
1146
+ turn, year, eventIndex, department: dept,
1147
+ summary: data.summary,
1148
+ risks: data.risks,
1149
+ recommendedActions: data.recommendedActions,
1150
+ citations: cites.length,
1151
+ toolCount: Array.isArray(data.forgedTools) ? data.forgedTools.length : 0,
1152
+ });
1153
+ for (const c of cites) {
1154
+ slot.citations.push({ ...c, department: dept, turn });
1155
+ }
1156
+ }
1157
+ else if (type === 'forge_attempt') {
1158
+ slot.forges.push({ turn, year, eventIndex, ...data });
1159
+ }
1160
+ else if (type === 'agent_reactions') {
1161
+ slot.agentReactions.push({ turn, year, reactions: data.reactions, totalReactions: data.totalReactions });
1162
+ }
1163
+ else if (type === 'promotion') {
1164
+ slot.promotions.push({ ...data });
1165
+ }
1166
+ else if (type === 'colony_snapshot') {
1167
+ slot.colonySnapshots.push({ turn, year, ...data });
1168
+ }
1169
+ }
1170
+ const leaders = [...byLeader.entries()].map(([name, slot]) => ({ name, ...slot }));
1171
+ res.writeHead(200, { ...corsHeaders, 'Content-Type': 'application/json' });
1172
+ res.end(JSON.stringify({
1173
+ results,
1174
+ verdict,
1175
+ isComplete,
1176
+ turnsCompleted: Math.floor(turns),
1177
+ totalEvents: simEvents.length,
1178
+ // New: structured per-leader payloads built from the SSE buffer
1179
+ leaders,
1180
+ }));
1181
+ return;
1182
+ }
1183
+ if (req.url === '/rate-limit' && req.method === 'GET') {
1184
+ const clientIp = IpRateLimiter.getIp(req);
1185
+ if (!rateLimiter) {
1186
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1187
+ res.end(JSON.stringify({ unlimited: true, ip: clientIp }));
1188
+ return;
1189
+ }
1190
+ const status = rateLimiter.check(clientIp);
1191
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1192
+ res.end(JSON.stringify({ ip: clientIp, ...status, resetAtISO: new Date(status.resetAt).toISOString() }));
1193
+ return;
1194
+ }
1195
+ // Cross-run schema-retry aggregate for production reliability
1196
+ // telemetry. Reads the rotating ring of the last N completed runs
1197
+ // and rolls up calls/attempts/fallbacks per Zod schema so operators
1198
+ // can answer "is this model retrying too much on CommanderDecision?"
1199
+ // without scraping individual run results.
1200
+ //
1201
+ // Query params:
1202
+ // ?limit=N — only aggregate the last N runs from the ring
1203
+ // Health endpoint — lightweight liveness + version check. Used by
1204
+ // monitors + CI/CD smoke tests to confirm the server came up cleanly
1205
+ // and which paracosm build is running. Cheap to hit: no LLM calls,
1206
+ // no ring-buffer iteration, just the current counters.
1207
+ if (req.url === '/health' && req.method === 'GET') {
1208
+ res.writeHead(200, {
1209
+ 'Content-Type': 'application/json',
1210
+ // Cloudflare sits in front; make sure a stale health snapshot
1211
+ // does not get cached at the edge and mislead monitors.
1212
+ 'Cache-Control': 'no-store, max-age=0',
1213
+ ...corsHeaders,
1214
+ });
1215
+ res.end(JSON.stringify({
1216
+ status: 'ok',
1217
+ version: PARACOSM_VERSION,
1218
+ uptimeSeconds: Math.round(process.uptime()),
1219
+ runCount: retryRing.length,
1220
+ }));
1221
+ return;
1222
+ }
1223
+ // ── Stored sessions: save / list / replay ───────────────────────
1224
+ //
1225
+ // Lets visitors replay a previously-saved demo via SSE instead of
1226
+ // triggering a fresh LLM-powered run. Save is gated by ADMIN_WRITE
1227
+ // (existing flag) — admin runs a good demo, hits POST /admin/sessions/save,
1228
+ // the in-memory event buffer (with per-event wall-clock timestamps)
1229
+ // gets written to SQLite. Public /sessions returns the metadata
1230
+ // listing; public /sessions/:id/replay streams the events back at
1231
+ // the original pacing (or accelerated via ?speed=N).
1232
+ if (req.url === '/admin/sessions/save' && req.method === 'POST') {
1233
+ if (!adminWrite) {
1234
+ res.writeHead(403, { 'Content-Type': 'application/json', ...corsHeaders });
1235
+ res.end(JSON.stringify({ error: 'ADMIN_WRITE not enabled on this server' }));
1236
+ return;
1237
+ }
1238
+ if (!sessionStore) {
1239
+ res.writeHead(503, { 'Content-Type': 'application/json', ...corsHeaders });
1240
+ res.end(JSON.stringify({ error: 'session store unavailable' }));
1241
+ return;
1242
+ }
1243
+ if (eventBuffer.length === 0) {
1244
+ res.writeHead(400, { 'Content-Type': 'application/json', ...corsHeaders });
1245
+ res.end(JSON.stringify({ error: 'no buffered events to save' }));
1246
+ return;
1247
+ }
1248
+ try {
1249
+ // Build the timestamped event array from the parallel buffers.
1250
+ // For events whose timestamp is 0 (rehydrated from disk pre-
1251
+ // timestamp-tracking), use the next known timestamp so replay
1252
+ // pacing stays monotonic instead of bunching at the start.
1253
+ const now = Date.now();
1254
+ const events = eventBuffer.map((sse, i) => ({
1255
+ ts: eventTimestamps[i] || now,
1256
+ sse,
1257
+ }));
1258
+ const result = sessionStore.saveSession(events);
1259
+ res.writeHead(200, { 'Content-Type': 'application/json', ...corsHeaders });
1260
+ res.end(JSON.stringify({
1261
+ ...result,
1262
+ eventCount: events.length,
1263
+ totalStored: sessionStore.count(),
1264
+ }));
1265
+ }
1266
+ catch (err) {
1267
+ res.writeHead(500, { 'Content-Type': 'application/json', ...corsHeaders });
1268
+ res.end(JSON.stringify({ error: String(err) }));
1269
+ }
1270
+ return;
1271
+ }
1272
+ if (req.url === '/sessions' && req.method === 'GET') {
1273
+ if (!sessionStore) {
1274
+ res.writeHead(503, { 'Content-Type': 'application/json', ...corsHeaders });
1275
+ res.end(JSON.stringify({ error: 'session store unavailable' }));
1276
+ return;
1277
+ }
1278
+ try {
1279
+ res.writeHead(200, { 'Content-Type': 'application/json', ...corsHeaders });
1280
+ res.end(JSON.stringify({ sessions: sessionStore.listSessions() }));
1281
+ }
1282
+ catch (err) {
1283
+ res.writeHead(500, { 'Content-Type': 'application/json', ...corsHeaders });
1284
+ res.end(JSON.stringify({ error: String(err) }));
1285
+ }
1286
+ return;
1287
+ }
1288
+ // Session metadata-only probe — `GET /sessions/:id` (no `/replay`
1289
+ // suffix) returns the session meta without the full event stream
1290
+ // so the client can validate a replay id before opening an
1291
+ // EventSource. Without this, a bogus ?replay=X URL produced an
1292
+ // invisible 404-retry loop on the SSE layer with no user signal.
1293
+ if (req.url?.startsWith('/sessions/') && !req.url.endsWith('/replay') && req.method === 'GET') {
1294
+ if (!sessionStore) {
1295
+ res.writeHead(503, { 'Content-Type': 'application/json', ...corsHeaders });
1296
+ res.end(JSON.stringify({ error: 'session store unavailable' }));
1297
+ return;
1298
+ }
1299
+ const id = req.url.replace(/^\/sessions\//, '').split('?')[0];
1300
+ if (!id || id.includes('/')) {
1301
+ res.writeHead(400, { 'Content-Type': 'application/json', ...corsHeaders });
1302
+ res.end(JSON.stringify({ error: 'invalid session id' }));
1303
+ return;
1304
+ }
1305
+ const session = sessionStore.getSession(id);
1306
+ if (!session) {
1307
+ res.writeHead(404, { 'Content-Type': 'application/json', ...corsHeaders });
1308
+ res.end(JSON.stringify({ error: 'session not found', id }));
1309
+ return;
1310
+ }
1311
+ res.writeHead(200, { 'Content-Type': 'application/json', ...corsHeaders });
1312
+ res.end(JSON.stringify({ meta: session.meta }));
1313
+ return;
1314
+ }
1315
+ if (req.url?.startsWith('/sessions/') && req.url.endsWith('/replay') && req.method === 'GET') {
1316
+ if (!sessionStore) {
1317
+ res.writeHead(503, { 'Content-Type': 'application/json', ...corsHeaders });
1318
+ res.end(JSON.stringify({ error: 'session store unavailable' }));
1319
+ return;
1320
+ }
1321
+ const url = new URL(req.url, 'http://localhost');
1322
+ const id = url.pathname.replace(/^\/sessions\//, '').replace(/\/replay$/, '');
1323
+ const speedRaw = url.searchParams.get('speed');
1324
+ const speed = Math.max(0.25, Math.min(50, speedRaw ? parseFloat(speedRaw) || 1 : 1));
1325
+ const session = sessionStore.getSession(id);
1326
+ if (!session) {
1327
+ res.writeHead(404, { 'Content-Type': 'application/json', ...corsHeaders });
1328
+ res.end(JSON.stringify({ error: 'session not found', id }));
1329
+ return;
1330
+ }
1331
+ // SSE stream: same headers as /events. Replays on the original
1332
+ // wall-clock pacing scaled by `speed` (1 = real-time, 4 = 4x
1333
+ // faster, 0.5 = half speed). Closes the response when done.
1334
+ res.writeHead(200, {
1335
+ 'Content-Type': 'text/event-stream',
1336
+ 'Cache-Control': 'no-cache, no-store, no-transform',
1337
+ Connection: 'keep-alive',
1338
+ 'X-Accel-Buffering': 'no',
1339
+ ...corsHeaders,
1340
+ });
1341
+ let cancelled = false;
1342
+ req.on('close', () => { cancelled = true; });
1343
+ void (async () => {
1344
+ let prevTs = session.events[0]?.ts ?? 0;
1345
+ for (const ev of session.events) {
1346
+ if (cancelled)
1347
+ return;
1348
+ const delay = Math.max(0, (ev.ts - prevTs) / speed);
1349
+ if (delay > 0)
1350
+ await new Promise(r => setTimeout(r, delay));
1351
+ prevTs = ev.ts;
1352
+ try {
1353
+ res.write(ev.sse);
1354
+ }
1355
+ catch {
1356
+ return;
1357
+ }
1358
+ }
1359
+ try {
1360
+ res.end();
1361
+ }
1362
+ catch { /* socket already closed */ }
1363
+ })();
1364
+ return;
1365
+ }
1366
+ if (req.url?.startsWith('/retry-stats') && req.method === 'GET') {
1367
+ const url = new URL(req.url, 'http://localhost');
1368
+ const limitRaw = url.searchParams.get('limit');
1369
+ const limit = limitRaw ? Math.max(1, Math.min(RETRY_RING_MAX, parseInt(limitRaw, 10) || RETRY_RING_MAX)) : undefined;
1370
+ const schemaWindow = limit ? retryRing.slice(-limit) : retryRing;
1371
+ const forgeWindow = limit ? forgeRing.slice(-limit) : forgeRing;
1372
+ const cacheWindow = limit ? cacheRing.slice(-limit) : cacheRing;
1373
+ const providerErrorWindow = limit ? providerErrorRing.slice(-limit) : providerErrorRing;
1374
+ const schemaAgg = aggregateSchemaRetries(schemaWindow);
1375
+ const forgeAgg = aggregateForgeStats(forgeWindow);
1376
+ const cacheAgg = aggregateCacheStats(cacheWindow);
1377
+ const providerErrorAgg = aggregateProviderErrors(providerErrorWindow);
1378
+ res.writeHead(200, { 'Content-Type': 'application/json', ...corsHeaders });
1379
+ res.end(JSON.stringify({
1380
+ ...schemaAgg,
1381
+ forges: forgeAgg,
1382
+ caches: cacheAgg,
1383
+ providerErrors: providerErrorAgg,
1384
+ }));
1385
+ return;
1386
+ }
1387
+ if (req.url === '/setup' && req.method === 'POST') {
1388
+ try {
1389
+ const config = JSON.parse(await readBody(req));
1390
+ // Rate limit check: bypass when user provides their own API keys
1391
+ const hasUserKeys = !!(config.apiKey || config.anthropicKey);
1392
+ if (rateLimiter && !hasUserKeys) {
1393
+ const ip = IpRateLimiter.getIp(req);
1394
+ const { allowed, remaining, limit } = rateLimiter.check(ip);
1395
+ if (!allowed) {
1396
+ console.log(` [rate-limit] Blocked ${ip} (${limit}/${limit} used)`);
1397
+ res.writeHead(429, {
1398
+ 'Content-Type': 'application/json',
1399
+ 'Retry-After': '86400',
1400
+ 'X-RateLimit-Limit': String(limit),
1401
+ 'X-RateLimit-Remaining': '0',
1402
+ });
1403
+ res.end(JSON.stringify({
1404
+ error: `Rate limit exceeded. Maximum ${limit} simulations per day. Add your own API keys in Settings to remove this limit.`,
1405
+ limit,
1406
+ remaining: 0,
1407
+ }));
1408
+ return;
1409
+ }
1410
+ rateLimiter.record(ip);
1411
+ console.log(` [rate-limit] ${ip}: ${remaining - 1} remaining of ${limit}`);
1412
+ }
1413
+ else if (hasUserKeys) {
1414
+ console.log(` [rate-limit] Bypassed — user provided API keys`);
1415
+ }
1416
+ if (!config.leaders || config.leaders.length < 2) {
1417
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1418
+ res.end(JSON.stringify({ error: 'Two leaders required' }));
1419
+ return;
1420
+ }
1421
+ simConfig = normalizeSimulationConfig(config);
1422
+ // Demo-mode enforcement: clamp only when the run bills against
1423
+ // the host's provider keys AND the server is in hosted-demo
1424
+ // mode. On local dev (PARACOSM_HOSTED_DEMO unset) env keys
1425
+ // belong to the operator, so we trust the tiered defaults and
1426
+ // any model overrides the client sent. On the hosted Linode
1427
+ // this variable is set, so env-only requests get clamped.
1428
+ const isHostedDemo = serverMode === 'hosted_demo';
1429
+ if (!hasUserKeys && isHostedDemo) {
1430
+ simConfig = applyDemoCaps(simConfig);
1431
+ console.log(` [demo-mode] Capped run: turns=${simConfig.turns} pop=${simConfig.initialPopulation} ` +
1432
+ `depts=${simConfig.activeDepartments.length} models=${simConfig.models.commander}`);
1433
+ }
1434
+ const runRecord = createRunRecord({
1435
+ scenarioId: activeScenario.id,
1436
+ scenarioVersion: activeScenario.version,
1437
+ leaderConfigHash: hashLeaderConfig({
1438
+ leaders: simConfig.leaders,
1439
+ turns: simConfig.turns,
1440
+ seed: simConfig.seed,
1441
+ }),
1442
+ economicsProfile: simConfig.economics.id,
1443
+ sourceMode: serverMode,
1444
+ createdBy: hasUserKeys ? 'user' : 'anonymous',
1445
+ });
1446
+ try {
1447
+ await runHistoryStore.insertRun(runRecord);
1448
+ }
1449
+ catch (error) {
1450
+ console.warn('[run-history] insert failed:', error);
1451
+ }
1452
+ // Key-scope safety: snapshot the env values we're about to mutate
1453
+ // so we can restore them when THIS sim ends. Without this, user A's
1454
+ // key persists in process.env after their sim completes; user B's
1455
+ // subsequent sim then silently uses A's key if B didn't provide one.
1456
+ // The restore runs in the startWithConfig().finally() below.
1457
+ const envKeysToRestore = [];
1458
+ const scopeKey = (envName, userValue) => {
1459
+ if (!userValue || userValue.includes('...'))
1460
+ return;
1461
+ envKeysToRestore.push([envName, env[envName]]);
1462
+ env[envName] = userValue;
1463
+ };
1464
+ scopeKey('OPENAI_API_KEY', simConfig.apiKey);
1465
+ scopeKey('ANTHROPIC_API_KEY', simConfig.anthropicKey);
1466
+ scopeKey('SERPER_API_KEY', simConfig.serperKey);
1467
+ scopeKey('FIRECRAWL_API_KEY', simConfig.firecrawlKey);
1468
+ scopeKey('TAVILY_API_KEY', simConfig.tavilyKey);
1469
+ scopeKey('COHERE_API_KEY', simConfig.cohereKey);
1470
+ const restoreEnv = () => {
1471
+ for (const [name, prior] of envKeysToRestore) {
1472
+ if (prior === undefined)
1473
+ delete env[name];
1474
+ else
1475
+ env[name] = prior;
1476
+ }
1477
+ };
1478
+ // If a run is already in flight, abort it before starting the
1479
+ // new one. Previously /setup silently no-op'd on simRunning,
1480
+ // which left the old sim draining API credits while the user
1481
+ // thought their new config had taken effect. The orchestrator
1482
+ // handles AbortSignal via its finally block in startWithConfig
1483
+ // (resets simRunning + activeSimAbortController), so awaiting
1484
+ // that unwind before starting ensures the event buffer clear +
1485
+ // new config take effect cleanly.
1486
+ if (simRunning && activeSimAbortController) {
1487
+ console.log(` [setup] Aborting in-flight sim before launching new one`);
1488
+ activeSimAbortController.abort();
1489
+ // Wait up to 5s for the previous run's finally block to run.
1490
+ // Without this, the orchestrator's finally could reset
1491
+ // simRunning AFTER the new startWithConfig has already set
1492
+ // it, then the watchdog would never re-arm on the new sim.
1493
+ const waitStart = Date.now();
1494
+ while (simRunning && Date.now() - waitStart < 5000) {
1495
+ await new Promise(resolve => setTimeout(resolve, 50));
1496
+ }
1497
+ }
1498
+ // Broadcast the scenario about to run BEFORE kicking off the
1499
+ // simulation. Closes the loop on "I uploaded Mercury JSON but
1500
+ // the run looked like Mars" — the dashboard can render a
1501
+ // prominent 'Running: Mars Genesis' banner so the user sees
1502
+ // immediately which scenario is active vs what's in their
1503
+ // editor.
1504
+ broadcast('active_scenario', {
1505
+ id: activeScenario.id,
1506
+ name: activeScenario.labels?.name ?? activeScenario.id,
1507
+ settlementNoun: activeScenario.labels?.settlementNoun,
1508
+ populationNoun: activeScenario.labels?.populationNoun,
1509
+ departments: activeScenario.departments?.length ?? 0,
1510
+ });
1511
+ console.log(` Running scenario: "${activeScenario.labels?.name ?? activeScenario.id}" (${activeScenario.id})`);
1512
+ // Start the sim and restore env when it finishes — prevents the
1513
+ // caller's keys from leaking into any subsequent /setup from a
1514
+ // different user that doesn't pass their own keys.
1515
+ marsServer.startWithConfig(simConfig).finally(restoreEnv);
1516
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1517
+ res.end(JSON.stringify({
1518
+ redirect: '/sim',
1519
+ scenarioId: activeScenario.id,
1520
+ scenarioName: activeScenario.labels?.name,
1521
+ run: {
1522
+ id: runRecord.runId,
1523
+ sourceMode: runRecord.sourceMode,
1524
+ economicsProfile: runRecord.economicsProfile,
1525
+ },
1526
+ }));
1527
+ }
1528
+ catch (error) {
1529
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1530
+ res.end(JSON.stringify({ error: String(error) }));
1531
+ }
1532
+ return;
1533
+ }
1534
+ if (req.url === '/about') {
1535
+ res.writeHead(302, { Location: '/sim?tab=about' });
1536
+ res.end();
1537
+ return;
1538
+ }
1539
+ // Serve brand assets
1540
+ if (req.url?.split('?')[0].startsWith('/brand/')) {
1541
+ const brandPath = resolve(__dirname, '..', '..', 'assets', req.url.split('?')[0].replace('/brand/', ''));
1542
+ if (existsSync(brandPath)) {
1543
+ const ext = brandPath.split('.').pop() || '';
1544
+ const types = { svg: 'image/svg+xml', png: 'image/png', jpg: 'image/jpeg', css: 'text/css', js: 'application/javascript', woff2: 'font/woff2' };
1545
+ res.writeHead(200, { 'Content-Type': types[ext] || 'application/octet-stream', 'Cache-Control': 'public, max-age=300, s-maxage=60' });
1546
+ res.end(readFileSync(brandPath));
1547
+ return;
1548
+ }
1549
+ }
1550
+ if (req.url === '/favicon.svg' || req.url === '/favicon.png' || req.url === '/favicon.ico' || req.url === '/icon.svg' || req.url === '/apple-touch-icon.png') {
1551
+ try {
1552
+ const assetsDir = resolve(__dirname, '..', '..', 'assets');
1553
+ const favDir = resolve(assetsDir, 'favicons');
1554
+ // Apple touch icon
1555
+ if (req.url === '/apple-touch-icon.png') {
1556
+ const p = resolve(favDir, 'favicon-180.png');
1557
+ if (existsSync(p)) {
1558
+ res.writeHead(200, { 'Content-Type': 'image/png', 'Cache-Control': 'public, max-age=86400' });
1559
+ res.end(readFileSync(p));
1560
+ return;
1561
+ }
1562
+ }
1563
+ // PNG routes: serve 32px PNG
1564
+ if (req.url === '/favicon.png' || req.url === '/favicon.ico') {
1565
+ const p = resolve(favDir, 'favicon-32.png');
1566
+ if (existsSync(p)) {
1567
+ res.writeHead(200, { 'Content-Type': 'image/png', 'Cache-Control': 'public, max-age=86400' });
1568
+ res.end(readFileSync(p));
1569
+ return;
1570
+ }
1571
+ }
1572
+ // SVG routes
1573
+ const svgPath = resolve(favDir, 'icon.svg');
1574
+ const fallbackSvg = resolve(assetsDir, 'mars-genesis-icon.svg');
1575
+ const iconPath = existsSync(svgPath) ? svgPath : existsSync(fallbackSvg) ? fallbackSvg : null;
1576
+ if (iconPath) {
1577
+ res.writeHead(200, { 'Content-Type': 'image/svg+xml', 'Cache-Control': 'public, max-age=86400' });
1578
+ res.end(readFileSync(iconPath, 'utf-8'));
1579
+ }
1580
+ else {
1581
+ res.writeHead(404);
1582
+ res.end();
1583
+ }
1584
+ }
1585
+ catch {
1586
+ res.writeHead(404);
1587
+ res.end();
1588
+ }
1589
+ return;
1590
+ }
1591
+ // ---------------------------------------------------------------------------
1592
+ // Static file serving
1593
+ // ---------------------------------------------------------------------------
1594
+ const distDir = resolve(__dirname, 'dashboard/dist');
1595
+ const hasViteBuild = existsSync(resolve(distDir, 'index.html'));
1596
+ const pathname = (req.url || '/').split('?')[0];
1597
+ // Landing page at /
1598
+ if (pathname === '/' || pathname === '/index.html') {
1599
+ const landingPath = resolve(__dirname, 'dashboard/landing.html');
1600
+ if (existsSync(landingPath)) {
1601
+ // Inject the package version into the schema.org LD+JSON block
1602
+ // so search engines and social previews show the real current
1603
+ // release instead of the stale literal that sat there
1604
+ // (`0.1.0` lingered in the HTML long after we shipped 0.4.x).
1605
+ // The placeholder is what lives on disk; we swap it per-request
1606
+ // since it's a one-line string replace on a cached template.
1607
+ const html = readFileSync(landingPath, 'utf-8').replace(/__PARACOSM_VERSION__/g, PARACOSM_VERSION);
1608
+ res.writeHead(200, { 'Content-Type': 'text/html', 'Cache-Control': 'no-cache' });
1609
+ res.end(html);
1610
+ return;
1611
+ }
1612
+ }
1613
+ // API docs (TypeDoc generated)
1614
+ if (pathname.startsWith('/docs')) {
1615
+ const docsDir = resolve(__dirname, '..', '..', 'docs', 'api');
1616
+ if (pathname === '/docs' || pathname === '/docs/') {
1617
+ res.writeHead(302, { Location: '/docs/modules.html' });
1618
+ res.end();
1619
+ return;
1620
+ }
1621
+ let docPath = pathname.replace('/docs', '');
1622
+ if (!docPath || docPath === '/')
1623
+ docPath = '/modules.html';
1624
+ const filePath = resolve(docsDir, docPath.startsWith('/') ? docPath.slice(1) : docPath);
1625
+ try {
1626
+ const { statSync } = await import('node:fs');
1627
+ const stat = statSync(filePath);
1628
+ if (stat.isFile()) {
1629
+ const ext = filePath.split('.').pop() || '';
1630
+ const mimeTypes = {
1631
+ html: 'text/html', css: 'text/css', js: 'application/javascript',
1632
+ svg: 'image/svg+xml', png: 'image/png', json: 'application/json',
1633
+ jpg: 'image/jpeg', gif: 'image/gif', woff: 'font/woff', woff2: 'font/woff2',
1634
+ };
1635
+ if (ext === 'html') {
1636
+ // Inject Paracosm theme into TypeDoc HTML
1637
+ let html = readFileSync(filePath, 'utf-8');
1638
+ // Rewrite relative asset paths to absolute /docs/ paths
1639
+ html = html.replace(/href="\.\.\/assets\//g, 'href="/docs/assets/');
1640
+ html = html.replace(/src="\.\.\/assets\//g, 'src="/docs/assets/');
1641
+ html = html.replace(/href="assets\//g, 'href="/docs/assets/');
1642
+ html = html.replace(/src="assets\//g, 'src="/docs/assets/');
1643
+ html = html.replace(/href="\.\.\/media\//g, 'href="/docs/media/');
1644
+ html = html.replace(/src="\.\.\/media\//g, 'src="/docs/media/');
1645
+ html = html.replace(/href="media\//g, 'href="/docs/media/');
1646
+ html = html.replace(/src="media\//g, 'src="/docs/media/');
1647
+ // TypeDoc toolbar + page title hidden via CSS (kept in DOM so JS doesn't crash)
1648
+ // Add our CSS override + fonts + favicon + inline mobile styles
1649
+ html = html.replace('</head>', `<link rel="icon" href="/favicon.png" sizes="32x32"><link rel="icon" type="image/svg+xml" href="/icon.svg"><link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet"><link rel="stylesheet" href="/docs/assets/paracosm-override.css">
1650
+ <style>
1651
+ /* Hamburger button (hidden on desktop) */
1652
+ .pdh-hamburger{display:none;background:none;border:1px solid var(--color-text-aside);border-radius:6px;width:32px;height:32px;cursor:pointer;flex-direction:column;align-items:center;justify-content:center;gap:4px;padding:7px;flex-shrink:0;opacity:.6;transition:opacity .2s}
1653
+ .pdh-hamburger:hover{opacity:1}
1654
+ .pdh-hamburger span{display:block;width:16px;height:2px;background:var(--color-text);border-radius:1px;transition:transform .25s,opacity .25s}
1655
+ .pdh-hamburger.open span:nth-child(1){transform:translateY(6px) rotate(45deg)}
1656
+ .pdh-hamburger.open span:nth-child(2){opacity:0}
1657
+ .pdh-hamburger.open span:nth-child(3){transform:translateY(-6px) rotate(-45deg)}
1658
+ /* Mobile nav dropdown (site links, not API sidebar) */
1659
+ .pdh-mobile-nav{display:none;position:fixed;top:44px;left:0;right:0;z-index:99999;background:var(--color-background);border-bottom:1px solid var(--color-background-active);padding:8px 16px;flex-direction:column;gap:0}
1660
+ .pdh-mobile-nav.open{display:flex}
1661
+ .pdh-mobile-nav a{display:block;padding:12px 8px;font-size:15px;font-weight:500;color:var(--color-text-aside);text-decoration:none;border-bottom:1px solid var(--color-background-active);font-family:'Inter',system-ui,sans-serif}
1662
+ .pdh-mobile-nav a:last-child{border-bottom:none}
1663
+ .pdh-mobile-nav a:hover{color:var(--color-text)}
1664
+ @media(max-width:1100px){
1665
+ header.tsd-page-toolbar{height:0!important;overflow:hidden!important;visibility:hidden!important;padding:0!important;margin:0!important;border:none!important}
1666
+ .container-main{display:block!important;grid-template-columns:none!important}
1667
+ .col-sidebar{display:none!important}
1668
+ .pdh-hamburger{display:flex}
1669
+ .pdh-right a,.pdh-right .pdh-search{display:none!important}
1670
+ }
1671
+ </style></head>`);
1672
+ // Inject nav header after <body>
1673
+ html = html.replace(/<body[^>]*>/, `$&
1674
+ <div class="paracosm-docs-header">
1675
+ <div class="pdh-left">
1676
+ <a href="/" style="display:flex;align-items:center;text-decoration:none" aria-label="Paracosm home">
1677
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="22" height="22" style="margin-right:8px;flex-shrink:0;display:block" role="img" aria-label="Paracosm"><style>.ph{animation:ph-p 4s ease-in-out infinite}.pg{animation:ph-g 4s ease-in-out infinite}@keyframes ph-p{0%,100%{opacity:1}50%{opacity:.75}}@keyframes ph-g{0%,100%{opacity:.06}50%{opacity:.15}}@media(prefers-reduced-motion:reduce){.ph,.pg{animation:none!important}}</style><line x1="32" y1="32" x2="37.63" y2="10.98" stroke="#f5f0e4" stroke-width="1.6" opacity=".5"/><line x1="32" y1="32" x2="53.02" y2="26.37" stroke="#f5f0e4" stroke-width="1.6" opacity=".5"/><line x1="32" y1="32" x2="47.39" y2="47.39" stroke="#f5f0e4" stroke-width="1.6" opacity=".5"/><line x1="32" y1="32" x2="26.37" y2="53.02" stroke="#f5f0e4" stroke-width="1.6" opacity=".5"/><line x1="32" y1="32" x2="10.98" y2="37.63" stroke="#f5f0e4" stroke-width="1.6" opacity=".5"/><line x1="32" y1="32" x2="16.61" y2="16.61" stroke="#f5f0e4" stroke-width="1.6" opacity=".5"/><line x1="37.63" y1="10.98" x2="47.39" y2="47.39" stroke="#f5f0e4" stroke-width="1.1" opacity=".18"/><line x1="53.02" y1="26.37" x2="26.37" y2="53.02" stroke="#f5f0e4" stroke-width="1.1" opacity=".18"/><line x1="47.39" y1="47.39" x2="10.98" y2="37.63" stroke="#f5f0e4" stroke-width="1.1" opacity=".18"/><line x1="26.37" y1="53.02" x2="16.61" y2="16.61" stroke="#f5f0e4" stroke-width="1.1" opacity=".18"/><line x1="10.98" y1="37.63" x2="37.63" y2="10.98" stroke="#f5f0e4" stroke-width="1.1" opacity=".18"/><line x1="16.61" y1="16.61" x2="53.02" y2="26.37" stroke="#f5f0e4" stroke-width="1.1" opacity=".18"/><circle class="pg" cx="32" cy="32" r="9.2" fill="#e8b44a"/><circle class="ph" cx="32" cy="32" r="5.12" fill="#e8b44a"/><circle cx="37.63" cy="10.98" r="3.52" fill="#e06530"/><circle cx="53.02" cy="26.37" r="3.52" fill="#e8b44a"/><circle cx="47.39" cy="47.39" r="3.52" fill="#4ca8a8"/><circle cx="26.37" cy="53.02" r="3.52" fill="#e06530"/><circle cx="10.98" cy="37.63" r="3.52" fill="#4ca8a8"/><circle cx="16.61" cy="16.61" r="3.52" fill="#e8b44a"/></svg>
1678
+ <span class="pdh-brand">PARA<span style="color:#e8b44a">COSM</span></span>
1679
+ </a>
1680
+ <a href="https://agentos.sh" target="_blank" rel="noopener" class="pdh-tag">AGENTOS</a>
1681
+ <span class="pdh-sep">|</span>
1682
+ <span class="pdh-current">API Reference v${PARACOSM_VERSION}</span>
1683
+ </div>
1684
+ <div class="pdh-right">
1685
+ <a href="/">Home</a>
1686
+ <a href="/sim">Simulation</a>
1687
+ <a href="/docs">API Docs</a>
1688
+ <a href="https://github.com/framersai/paracosm" target="_blank" rel="noopener">GitHub</a>
1689
+ <a href="https://www.npmjs.com/package/paracosm" target="_blank" rel="noopener">npm</a>
1690
+ <button class="pdh-search" onclick="document.getElementById('tsd-search-trigger')?.click()" aria-label="Search docs"><svg width="14" height="14" viewBox="0 0 16 16" fill="none"><circle cx="7" cy="7" r="5.5" stroke="currentColor" stroke-width="1.5"/><line x1="11" y1="11" x2="15" y2="15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></button>
1691
+ <button class="pdh-theme" id="pdh-theme-toggle" aria-label="Toggle theme"></button>
1692
+ <button class="pdh-hamburger" id="pdh-hamburger" aria-label="Toggle menu"><span></span><span></span><span></span></button>
1693
+ </div>
1694
+ </div>
1695
+ <div class="pdh-mobile-nav" id="pdh-mobile-nav">
1696
+ <a href="/">Home</a>
1697
+ <a href="/sim">Simulation</a>
1698
+ <a href="/docs">API Docs</a>
1699
+ <a href="https://github.com/framersai/paracosm" target="_blank" rel="noopener">GitHub</a>
1700
+ <a href="https://www.npmjs.com/package/paracosm" target="_blank" rel="noopener">npm</a>
1701
+ <a href="https://agentos.sh" target="_blank" rel="noopener">AgentOS</a>
1702
+ <a href="https://wilds.ai/discord" target="_blank" rel="noopener">Discord</a>
1703
+ </div>
1704
+ <script>
1705
+ (function(){
1706
+ // Search: intercept showModal -> show() so CSS can position it as popover
1707
+ var d=document.getElementById('tsd-search');
1708
+ if(d){
1709
+ d.showModal=function(){d.show();d.style.position='fixed';d.style.top='54px';d.style.right='24px';d.style.left='auto';d.style.bottom='auto';d.style.width='420px';d.style.maxWidth='calc(100vw - 48px)';d.style.maxHeight='480px';d.style.margin='0';d.style.borderRadius='8px';d.style.boxShadow='0 12px 40px rgba(0,0,0,.5)';d.style.zIndex='99999';var i=d.querySelector('input');if(i)i.focus();};
1710
+ document.addEventListener('click',function(e){if(d.open&&!d.contains(e.target)&&!e.target.closest('.pdh-search'))d.close();});
1711
+ }
1712
+ // Theme toggle
1713
+ var btn=document.getElementById('pdh-theme-toggle');
1714
+ if(btn){
1715
+ function applyTheme(t){
1716
+ document.documentElement.dataset.theme=t;
1717
+ localStorage.setItem('tsd-theme',t);
1718
+ btn.textContent=t==='dark'?'\\u2600':'\\u263D';
1719
+ }
1720
+ var saved=localStorage.getItem('tsd-theme')||'os';
1721
+ if(saved==='os') saved=window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light';
1722
+ applyTheme(saved);
1723
+ btn.addEventListener('click',function(){
1724
+ var next=document.documentElement.dataset.theme==='dark'?'light':'dark';
1725
+ applyTheme(next);
1726
+ });
1727
+ }
1728
+ // Hamburger: toggle mobile nav dropdown
1729
+ var hb=document.getElementById('pdh-hamburger');
1730
+ var mn=document.getElementById('pdh-mobile-nav');
1731
+ if(hb&&mn){
1732
+ hb.addEventListener('click',function(){
1733
+ hb.classList.toggle('open');
1734
+ mn.classList.toggle('open');
1735
+ });
1736
+ mn.querySelectorAll('a').forEach(function(a){
1737
+ a.addEventListener('click',function(){hb.classList.remove('open');mn.classList.remove('open');});
1738
+ });
1739
+ }
1740
+ })();
1741
+ </script>`);
1742
+ // Inject footer before </body>
1743
+ html = html.replace('</body>', `<div class="paracosm-docs-footer">
1744
+ <div class="pdf-links">
1745
+ <a href="https://agentos.sh">agentos.sh</a>
1746
+ <a href="https://github.com/framersai/paracosm">GitHub</a>
1747
+ <a href="https://www.npmjs.com/package/paracosm">npm</a>
1748
+ <a href="https://frame.dev">Frame.dev</a>
1749
+ <a href="https://manic.agency">Manic Agency</a>
1750
+ </div>
1751
+ <span><span style="font-family:'JetBrains Mono','SF Mono',Menlo,monospace;font-weight:700;letter-spacing:.08em;font-size:10px">PARA<span style="color:#e8b44a">COSM</span></span> &middot; Apache-2.0 &middot; <a href="https://manic.agency">Manic Agency</a> / <a href="https://frame.dev">Frame.dev</a></span>
1752
+ </div></body>`);
1753
+ res.writeHead(200, { 'Content-Type': 'text/html', 'Cache-Control': 'no-cache' });
1754
+ res.end(html);
1755
+ return;
1756
+ }
1757
+ const content = readFileSync(filePath);
1758
+ res.writeHead(200, {
1759
+ 'Content-Type': mimeTypes[ext] || 'application/octet-stream',
1760
+ 'Cache-Control': 'no-cache',
1761
+ });
1762
+ res.end(content);
1763
+ return;
1764
+ }
1765
+ // Directory: try index.html
1766
+ if (stat.isDirectory()) {
1767
+ const indexPath = resolve(filePath, 'index.html');
1768
+ if (existsSync(indexPath)) {
1769
+ const content = readFileSync(indexPath);
1770
+ res.writeHead(200, { 'Content-Type': 'text/html', 'Cache-Control': 'no-cache' });
1771
+ res.end(content);
1772
+ return;
1773
+ }
1774
+ }
1775
+ }
1776
+ catch { }
1777
+ }
1778
+ // Vite assets (CSS, JS, fonts)
1779
+ if (req.url?.startsWith('/assets/')) {
1780
+ const assetPath = resolve(distDir, req.url.slice(1));
1781
+ if (existsSync(assetPath)) {
1782
+ const ext = assetPath.split('.').pop();
1783
+ const mimeTypes = {
1784
+ js: 'application/javascript', css: 'text/css', svg: 'image/svg+xml',
1785
+ png: 'image/png', jpg: 'image/jpeg', woff2: 'font/woff2', woff: 'font/woff',
1786
+ };
1787
+ const content = readFileSync(assetPath);
1788
+ res.writeHead(200, {
1789
+ 'Content-Type': mimeTypes[ext || ''] || 'application/octet-stream',
1790
+ 'Cache-Control': 'public, max-age=31536000, immutable',
1791
+ });
1792
+ res.end(content);
1793
+ return;
1794
+ }
1795
+ }
1796
+ // Simulation dashboard at /sim (SPA)
1797
+ if (pathname === '/sim' || pathname.startsWith('/sim/') || pathname === '/sim/index.html') {
1798
+ if (hasViteBuild) {
1799
+ const html = readFileSync(resolve(distDir, 'index.html'), 'utf-8');
1800
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1801
+ res.end(html);
1802
+ return;
1803
+ }
1804
+ }
1805
+ res.writeHead(404);
1806
+ res.end('Not found');
1807
+ });
1808
+ // Clean up rate limiter on server close
1809
+ server.on('close', () => { if (rateLimiter)
1810
+ rateLimiter.destroy(); });
1811
+ const marsServer = Object.assign(server, {
1812
+ async startWithConfig(config) {
1813
+ // Clear previous run data
1814
+ clearEventBuffer();
1815
+ simConfig = config;
1816
+ simRunning = true;
1817
+ // Per-run AbortController. Held in a local so the finally block
1818
+ // only clears the global flag when it still points to *our* run.
1819
+ // Without the identity check, a slow cleanup on an old run could
1820
+ // null the active controller of a newer run that /setup just
1821
+ // started, breaking the disconnect watchdog's abort path.
1822
+ const controller = new AbortController();
1823
+ activeSimAbortController = controller;
1824
+ try {
1825
+ // Thread the currently-active scenario through to the pair
1826
+ // runner. Without this the runner defaults to Mars regardless
1827
+ // of which scenario the user compiled — the page title would
1828
+ // show the custom name but the simulation would run Mars
1829
+ // hooks + content.
1830
+ await startSimulations(config, broadcast, controller.signal, activeScenario);
1831
+ }
1832
+ finally {
1833
+ disarmDisconnectWatchdog();
1834
+ if (activeSimAbortController === controller) {
1835
+ simRunning = false;
1836
+ activeSimAbortController = null;
1837
+ }
1838
+ // Else: a newer run already replaced us. Leave simRunning and
1839
+ // activeSimAbortController alone so the new run's watchdog
1840
+ // continues to work.
1841
+ }
1842
+ },
1843
+ });
1844
+ return marsServer;
1845
+ }
1846
+ //# sourceMappingURL=server-app.js.map