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.
- package/README.md +186 -14
- package/config/leaders.example.json +32 -0
- package/config/leaders.json +32 -0
- package/dist/cli/cli-run-options.d.ts +11 -0
- package/dist/cli/cli-run-options.d.ts.map +1 -0
- package/dist/cli/cli-run-options.js +39 -0
- package/dist/cli/cli-run-options.js.map +1 -0
- package/dist/cli/compile-cli-options.d.ts +14 -0
- package/dist/cli/compile-cli-options.d.ts.map +1 -0
- package/dist/cli/compile-cli-options.js +48 -0
- package/dist/cli/compile-cli-options.js.map +1 -0
- package/dist/cli/compile.d.ts +11 -0
- package/dist/cli/compile.d.ts.map +1 -0
- package/dist/cli/compile.js +92 -0
- package/dist/cli/compile.js.map +1 -0
- package/dist/cli/custom-scenarios.d.ts +10 -0
- package/dist/cli/custom-scenarios.d.ts.map +1 -0
- package/dist/cli/custom-scenarios.js +93 -0
- package/dist/cli/custom-scenarios.js.map +1 -0
- package/dist/cli/leaders-resolver.d.ts +26 -0
- package/dist/cli/leaders-resolver.d.ts.map +1 -0
- package/dist/cli/leaders-resolver.js +66 -0
- package/dist/cli/leaders-resolver.js.map +1 -0
- package/dist/cli/pair-runner.d.ts +23 -0
- package/dist/cli/pair-runner.d.ts.map +1 -0
- package/dist/cli/pair-runner.js +225 -0
- package/dist/cli/pair-runner.js.map +1 -0
- package/dist/cli/rate-limiter.d.ts +78 -0
- package/dist/cli/rate-limiter.d.ts.map +1 -0
- package/dist/cli/rate-limiter.js +193 -0
- package/dist/cli/rate-limiter.js.map +1 -0
- package/dist/cli/retry-stats.d.ts +179 -0
- package/dist/cli/retry-stats.d.ts.map +1 -0
- package/dist/cli/retry-stats.js +167 -0
- package/dist/cli/retry-stats.js.map +1 -0
- package/dist/cli/run-a.d.ts +2 -0
- package/dist/cli/run-a.d.ts.map +1 -0
- package/dist/cli/run-a.js +13 -0
- package/dist/cli/run-a.js.map +1 -0
- package/dist/cli/run-b.d.ts +2 -0
- package/dist/cli/run-b.d.ts.map +1 -0
- package/dist/cli/run-b.js +13 -0
- package/dist/cli/run-b.js.map +1 -0
- package/dist/cli/run.d.ts +18 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +135 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/serve.d.ts +10 -0
- package/dist/cli/serve.d.ts.map +1 -0
- package/dist/cli/serve.js +71 -0
- package/dist/cli/serve.js.map +1 -0
- package/dist/cli/server/routes/platform-api.d.ts +8 -0
- package/dist/cli/server/routes/platform-api.d.ts.map +1 -0
- package/dist/cli/server/routes/platform-api.js +41 -0
- package/dist/cli/server/routes/platform-api.js.map +1 -0
- package/dist/cli/server/routes/public-demo.d.ts +4 -0
- package/dist/cli/server/routes/public-demo.d.ts.map +1 -0
- package/dist/cli/server/routes/public-demo.js +16 -0
- package/dist/cli/server/routes/public-demo.js.map +1 -0
- package/dist/cli/server/run-history-store.d.ts +8 -0
- package/dist/cli/server/run-history-store.d.ts.map +1 -0
- package/dist/cli/server/run-history-store.js +8 -0
- package/dist/cli/server/run-history-store.js.map +1 -0
- package/dist/cli/server/run-record.d.ts +14 -0
- package/dist/cli/server/run-record.d.ts.map +1 -0
- package/dist/cli/server/run-record.js +12 -0
- package/dist/cli/server/run-record.js.map +1 -0
- package/dist/cli/server/server-mode.d.ts +3 -0
- package/dist/cli/server/server-mode.d.ts.map +1 -0
- package/dist/cli/server/server-mode.js +8 -0
- package/dist/cli/server/server-mode.js.map +1 -0
- package/dist/cli/server-app.d.ts +46 -0
- package/dist/cli/server-app.d.ts.map +1 -0
- package/dist/cli/server-app.js +1846 -0
- package/dist/cli/server-app.js.map +1 -0
- package/dist/cli/session-store.d.ts +103 -0
- package/dist/cli/session-store.d.ts.map +1 -0
- package/dist/cli/session-store.js +254 -0
- package/dist/cli/session-store.js.map +1 -0
- package/dist/cli/session-title.d.ts +81 -0
- package/dist/cli/session-title.d.ts.map +1 -0
- package/dist/cli/session-title.js +229 -0
- package/dist/cli/session-title.js.map +1 -0
- package/dist/cli/sim-config.d.ts.map +1 -0
- package/dist/cli/sim-config.js.map +1 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/{src/cli → cli}/types.js.map +1 -1
- package/dist/engine/_template/index.d.ts.map +1 -0
- package/dist/engine/_template/index.js.map +1 -0
- package/dist/engine/compiler/cache.d.ts.map +1 -0
- package/dist/engine/compiler/cache.js.map +1 -0
- package/dist/engine/compiler/generate-director.d.ts.map +1 -0
- package/dist/engine/compiler/generate-director.js.map +1 -0
- package/dist/engine/compiler/generate-fingerprint.d.ts.map +1 -0
- package/dist/engine/compiler/generate-fingerprint.js.map +1 -0
- package/dist/engine/compiler/generate-milestones.d.ts.map +1 -0
- package/dist/engine/compiler/generate-milestones.js.map +1 -0
- package/dist/engine/compiler/generate-politics.d.ts.map +1 -0
- package/dist/engine/compiler/generate-politics.js.map +1 -0
- package/dist/engine/compiler/generate-progression.d.ts.map +1 -0
- package/dist/engine/compiler/generate-progression.js.map +1 -0
- package/dist/engine/compiler/generate-prompts.d.ts.map +1 -0
- package/dist/engine/compiler/generate-prompts.js.map +1 -0
- package/dist/engine/compiler/generate-reactions.d.ts.map +1 -0
- package/dist/engine/compiler/generate-reactions.js.map +1 -0
- package/dist/engine/compiler/index.d.ts.map +1 -0
- package/dist/engine/compiler/index.js.map +1 -0
- package/dist/engine/compiler/llm-invocations/generateValidatedCode.d.ts.map +1 -0
- package/dist/engine/compiler/llm-invocations/generateValidatedCode.js.map +1 -0
- package/dist/engine/compiler/llm-invocations/generateValidatedJson.d.ts.map +1 -0
- package/dist/engine/compiler/llm-invocations/generateValidatedJson.js.map +1 -0
- package/dist/engine/compiler/llm-invocations/generateValidatedObject.d.ts.map +1 -0
- package/dist/engine/compiler/llm-invocations/generateValidatedObject.js.map +1 -0
- package/dist/engine/compiler/llm-invocations/generateValidatedProse.d.ts.map +1 -0
- package/dist/engine/compiler/llm-invocations/generateValidatedProse.js.map +1 -0
- package/dist/engine/compiler/schemas/index.d.ts.map +1 -0
- package/dist/engine/compiler/schemas/index.js.map +1 -0
- package/dist/engine/compiler/schemas/milestones.d.ts.map +1 -0
- package/dist/engine/compiler/schemas/milestones.js.map +1 -0
- package/dist/engine/compiler/seed-ingestion.d.ts.map +1 -0
- package/dist/engine/compiler/seed-ingestion.js.map +1 -0
- package/dist/engine/compiler/telemetry.d.ts.map +1 -0
- package/dist/engine/compiler/telemetry.js.map +1 -0
- package/dist/engine/compiler/types.d.ts.map +1 -0
- package/dist/engine/compiler/types.js.map +1 -0
- package/dist/engine/compiler/validate.d.ts.map +1 -0
- package/dist/engine/compiler/validate.js.map +1 -0
- package/dist/engine/core/agent-generator.d.ts.map +1 -0
- package/dist/engine/core/agent-generator.js.map +1 -0
- package/dist/engine/core/kernel.d.ts.map +1 -0
- package/dist/engine/core/kernel.js.map +1 -0
- package/dist/engine/core/progression.d.ts.map +1 -0
- package/dist/engine/core/progression.js.map +1 -0
- package/dist/engine/core/rng.d.ts.map +1 -0
- package/dist/engine/core/rng.js.map +1 -0
- package/dist/engine/core/state.d.ts.map +1 -0
- package/dist/engine/core/state.js.map +1 -0
- package/dist/engine/effect-registry.d.ts.map +1 -0
- package/dist/engine/effect-registry.js.map +1 -0
- package/dist/engine/event-taxonomy.d.ts.map +1 -0
- package/dist/engine/event-taxonomy.js.map +1 -0
- package/dist/{src/engine → engine}/index.d.ts +2 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/{src/engine → engine}/index.js +5 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/lunar/effects.d.ts.map +1 -0
- package/dist/engine/lunar/effects.js.map +1 -0
- package/dist/engine/lunar/events.d.ts.map +1 -0
- package/dist/engine/lunar/events.js.map +1 -0
- package/dist/engine/lunar/fingerprint.d.ts.map +1 -0
- package/dist/engine/lunar/fingerprint.js.map +1 -0
- package/dist/engine/lunar/hooks.d.ts.map +1 -0
- package/dist/engine/lunar/hooks.js.map +1 -0
- package/dist/engine/lunar/index.d.ts.map +1 -0
- package/dist/engine/lunar/index.js.map +1 -0
- package/dist/engine/lunar/metrics.d.ts.map +1 -0
- package/dist/engine/lunar/metrics.js.map +1 -0
- package/dist/engine/lunar/milestones.d.ts.map +1 -0
- package/dist/engine/lunar/milestones.js.map +1 -0
- package/dist/engine/lunar/names.d.ts.map +1 -0
- package/dist/engine/lunar/names.js.map +1 -0
- package/dist/engine/lunar/politics.d.ts.map +1 -0
- package/dist/engine/lunar/politics.js.map +1 -0
- package/dist/engine/lunar/presets.d.ts.map +1 -0
- package/dist/engine/lunar/presets.js.map +1 -0
- package/dist/engine/lunar/progression-hooks.d.ts.map +1 -0
- package/dist/engine/lunar/progression-hooks.js.map +1 -0
- package/dist/engine/lunar/prompts.d.ts.map +1 -0
- package/dist/engine/lunar/prompts.js.map +1 -0
- package/dist/engine/lunar/reactions.d.ts.map +1 -0
- package/dist/engine/lunar/reactions.js.map +1 -0
- package/dist/engine/lunar/research-bundle.d.ts.map +1 -0
- package/dist/engine/lunar/research-bundle.js.map +1 -0
- package/dist/engine/mars/effects.d.ts.map +1 -0
- package/dist/engine/mars/effects.js.map +1 -0
- package/dist/engine/mars/events.d.ts.map +1 -0
- package/dist/engine/mars/events.js.map +1 -0
- package/dist/engine/mars/fingerprint.d.ts.map +1 -0
- package/dist/engine/mars/fingerprint.js.map +1 -0
- package/dist/engine/mars/hooks.d.ts.map +1 -0
- package/dist/engine/mars/hooks.js.map +1 -0
- package/dist/engine/mars/index.d.ts.map +1 -0
- package/dist/engine/mars/index.js.map +1 -0
- package/dist/engine/mars/metrics.d.ts.map +1 -0
- package/dist/engine/mars/metrics.js.map +1 -0
- package/dist/engine/mars/milestones.d.ts.map +1 -0
- package/dist/engine/mars/milestones.js.map +1 -0
- package/dist/engine/mars/names.d.ts.map +1 -0
- package/dist/engine/mars/names.js.map +1 -0
- package/dist/engine/mars/politics.d.ts.map +1 -0
- package/dist/engine/mars/politics.js.map +1 -0
- package/dist/engine/mars/presets.d.ts.map +1 -0
- package/dist/engine/mars/presets.js.map +1 -0
- package/dist/engine/mars/progression-hooks.d.ts.map +1 -0
- package/dist/engine/mars/progression-hooks.js.map +1 -0
- package/dist/engine/mars/prompts.d.ts.map +1 -0
- package/dist/engine/mars/prompts.js.map +1 -0
- package/dist/engine/mars/reactions.d.ts.map +1 -0
- package/dist/engine/mars/reactions.js.map +1 -0
- package/dist/engine/mars/research-bundle.d.ts.map +1 -0
- package/dist/engine/mars/research-bundle.js.map +1 -0
- package/dist/engine/metric-registry.d.ts.map +1 -0
- package/dist/engine/metric-registry.js.map +1 -0
- package/dist/engine/provider-resolver.d.ts.map +1 -0
- package/dist/{src/engine → engine}/provider-resolver.js +2 -1
- package/dist/engine/provider-resolver.js.map +1 -0
- package/dist/engine/types.d.ts.map +1 -0
- package/dist/{src/engine → engine}/types.js.map +1 -1
- package/dist/runtime/agent-memory.d.ts.map +1 -0
- package/dist/runtime/agent-memory.js.map +1 -0
- package/dist/runtime/agent-reactions.d.ts.map +1 -0
- package/dist/runtime/agent-reactions.js.map +1 -0
- package/dist/runtime/batch.d.ts.map +1 -0
- package/dist/runtime/batch.js.map +1 -0
- package/dist/runtime/chat-agents.d.ts.map +1 -0
- package/dist/runtime/chat-agents.js.map +1 -0
- package/dist/runtime/citations-catalog.d.ts.map +1 -0
- package/dist/runtime/citations-catalog.js.map +1 -0
- package/dist/runtime/commander-setup.d.ts.map +1 -0
- package/dist/runtime/commander-setup.js.map +1 -0
- package/dist/runtime/contracts.d.ts.map +1 -0
- package/dist/{src/runtime → runtime}/contracts.js.map +1 -1
- package/dist/runtime/cost-tracker.d.ts.map +1 -0
- package/dist/runtime/cost-tracker.js.map +1 -0
- package/dist/runtime/departments.d.ts.map +1 -0
- package/dist/runtime/departments.js.map +1 -0
- package/dist/runtime/director.d.ts.map +1 -0
- package/dist/runtime/director.js.map +1 -0
- package/dist/runtime/economics-profile.d.ts.map +1 -0
- package/dist/runtime/economics-profile.js.map +1 -0
- package/dist/runtime/emergent-setup.d.ts.map +1 -0
- package/dist/runtime/emergent-setup.js.map +1 -0
- package/dist/runtime/generic-fingerprint.d.ts.map +1 -0
- package/dist/runtime/generic-fingerprint.js.map +1 -0
- package/dist/runtime/hexaco-cues/trajectory.d.ts.map +1 -0
- package/dist/runtime/hexaco-cues/trajectory.js.map +1 -0
- package/dist/runtime/hexaco-cues/translation.d.ts.map +1 -0
- package/dist/runtime/hexaco-cues/translation.js.map +1 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/llm-invocations/generateValidatedObject.d.ts.map +1 -0
- package/dist/runtime/llm-invocations/generateValidatedObject.js.map +1 -0
- package/dist/runtime/llm-invocations/sendAndValidate.d.ts.map +1 -0
- package/dist/runtime/llm-invocations/sendAndValidate.js.map +1 -0
- package/dist/runtime/orchestrator.d.ts.map +1 -0
- package/dist/runtime/orchestrator.js.map +1 -0
- package/dist/runtime/output-writer.d.ts.map +1 -0
- package/dist/runtime/output-writer.js.map +1 -0
- package/dist/runtime/parsers.d.ts.map +1 -0
- package/dist/runtime/parsers.js.map +1 -0
- package/dist/runtime/pricing.d.ts.map +1 -0
- package/dist/runtime/pricing.js.map +1 -0
- package/dist/runtime/provider-errors.d.ts.map +1 -0
- package/dist/runtime/provider-errors.js.map +1 -0
- package/dist/runtime/reaction-step.d.ts.map +1 -0
- package/dist/runtime/reaction-step.js.map +1 -0
- package/dist/runtime/research/knowledge-base.d.ts.map +1 -0
- package/dist/runtime/research/knowledge-base.js.map +1 -0
- package/dist/runtime/research/research-memory.d.ts.map +1 -0
- package/dist/runtime/research/research-memory.js.map +1 -0
- package/dist/runtime/research/research.d.ts.map +1 -0
- package/dist/runtime/research/research.js.map +1 -0
- package/dist/runtime/research/scenario-research.d.ts.map +1 -0
- package/dist/runtime/research/scenario-research.js.map +1 -0
- package/dist/runtime/research/scenarios.d.ts.map +1 -0
- package/dist/runtime/research/scenarios.js.map +1 -0
- package/dist/runtime/runtime-helpers.d.ts.map +1 -0
- package/dist/runtime/runtime-helpers.js.map +1 -0
- package/dist/runtime/schemas/commander.d.ts.map +1 -0
- package/dist/runtime/schemas/commander.js.map +1 -0
- package/dist/runtime/schemas/department.d.ts.map +1 -0
- package/dist/runtime/schemas/department.js.map +1 -0
- package/dist/runtime/schemas/director.d.ts.map +1 -0
- package/dist/runtime/schemas/director.js.map +1 -0
- package/dist/runtime/schemas/index.d.ts.map +1 -0
- package/dist/runtime/schemas/index.js.map +1 -0
- package/dist/runtime/schemas/reactions.d.ts.map +1 -0
- package/dist/runtime/schemas/reactions.js.map +1 -0
- package/dist/runtime/schemas/verdict.d.ts.map +1 -0
- package/dist/runtime/schemas/verdict.js.map +1 -0
- package/dist/runtime/tool-ledger.d.ts.map +1 -0
- package/dist/runtime/tool-ledger.js.map +1 -0
- package/package.json +8 -2
- package/dist/src/cli/sim-config.d.ts.map +0 -1
- package/dist/src/cli/sim-config.js.map +0 -1
- package/dist/src/cli/types.d.ts.map +0 -1
- package/dist/src/engine/_template/index.d.ts.map +0 -1
- package/dist/src/engine/_template/index.js.map +0 -1
- package/dist/src/engine/compiler/cache.d.ts.map +0 -1
- package/dist/src/engine/compiler/cache.js.map +0 -1
- package/dist/src/engine/compiler/generate-director.d.ts.map +0 -1
- package/dist/src/engine/compiler/generate-director.js.map +0 -1
- package/dist/src/engine/compiler/generate-fingerprint.d.ts.map +0 -1
- package/dist/src/engine/compiler/generate-fingerprint.js.map +0 -1
- package/dist/src/engine/compiler/generate-milestones.d.ts.map +0 -1
- package/dist/src/engine/compiler/generate-milestones.js.map +0 -1
- package/dist/src/engine/compiler/generate-politics.d.ts.map +0 -1
- package/dist/src/engine/compiler/generate-politics.js.map +0 -1
- package/dist/src/engine/compiler/generate-progression.d.ts.map +0 -1
- package/dist/src/engine/compiler/generate-progression.js.map +0 -1
- package/dist/src/engine/compiler/generate-prompts.d.ts.map +0 -1
- package/dist/src/engine/compiler/generate-prompts.js.map +0 -1
- package/dist/src/engine/compiler/generate-reactions.d.ts.map +0 -1
- package/dist/src/engine/compiler/generate-reactions.js.map +0 -1
- package/dist/src/engine/compiler/index.d.ts.map +0 -1
- package/dist/src/engine/compiler/index.js.map +0 -1
- package/dist/src/engine/compiler/llm-invocations/generateValidatedCode.d.ts.map +0 -1
- package/dist/src/engine/compiler/llm-invocations/generateValidatedCode.js.map +0 -1
- package/dist/src/engine/compiler/llm-invocations/generateValidatedJson.d.ts.map +0 -1
- package/dist/src/engine/compiler/llm-invocations/generateValidatedJson.js.map +0 -1
- package/dist/src/engine/compiler/llm-invocations/generateValidatedObject.d.ts.map +0 -1
- package/dist/src/engine/compiler/llm-invocations/generateValidatedObject.js.map +0 -1
- package/dist/src/engine/compiler/llm-invocations/generateValidatedProse.d.ts.map +0 -1
- package/dist/src/engine/compiler/llm-invocations/generateValidatedProse.js.map +0 -1
- package/dist/src/engine/compiler/schemas/index.d.ts.map +0 -1
- package/dist/src/engine/compiler/schemas/index.js.map +0 -1
- package/dist/src/engine/compiler/schemas/milestones.d.ts.map +0 -1
- package/dist/src/engine/compiler/schemas/milestones.js.map +0 -1
- package/dist/src/engine/compiler/seed-ingestion.d.ts.map +0 -1
- package/dist/src/engine/compiler/seed-ingestion.js.map +0 -1
- package/dist/src/engine/compiler/telemetry.d.ts.map +0 -1
- package/dist/src/engine/compiler/telemetry.js.map +0 -1
- package/dist/src/engine/compiler/types.d.ts.map +0 -1
- package/dist/src/engine/compiler/types.js.map +0 -1
- package/dist/src/engine/compiler/validate.d.ts.map +0 -1
- package/dist/src/engine/compiler/validate.js.map +0 -1
- package/dist/src/engine/core/agent-generator.d.ts.map +0 -1
- package/dist/src/engine/core/agent-generator.js.map +0 -1
- package/dist/src/engine/core/kernel.d.ts.map +0 -1
- package/dist/src/engine/core/kernel.js.map +0 -1
- package/dist/src/engine/core/progression.d.ts.map +0 -1
- package/dist/src/engine/core/progression.js.map +0 -1
- package/dist/src/engine/core/rng.d.ts.map +0 -1
- package/dist/src/engine/core/rng.js.map +0 -1
- package/dist/src/engine/core/state.d.ts.map +0 -1
- package/dist/src/engine/core/state.js.map +0 -1
- package/dist/src/engine/effect-registry.d.ts.map +0 -1
- package/dist/src/engine/effect-registry.js.map +0 -1
- package/dist/src/engine/event-taxonomy.d.ts.map +0 -1
- package/dist/src/engine/event-taxonomy.js.map +0 -1
- package/dist/src/engine/index.d.ts.map +0 -1
- package/dist/src/engine/index.js.map +0 -1
- package/dist/src/engine/lunar/effects.d.ts.map +0 -1
- package/dist/src/engine/lunar/effects.js.map +0 -1
- package/dist/src/engine/lunar/events.d.ts.map +0 -1
- package/dist/src/engine/lunar/events.js.map +0 -1
- package/dist/src/engine/lunar/fingerprint.d.ts.map +0 -1
- package/dist/src/engine/lunar/fingerprint.js.map +0 -1
- package/dist/src/engine/lunar/hooks.d.ts.map +0 -1
- package/dist/src/engine/lunar/hooks.js.map +0 -1
- package/dist/src/engine/lunar/index.d.ts.map +0 -1
- package/dist/src/engine/lunar/index.js.map +0 -1
- package/dist/src/engine/lunar/metrics.d.ts.map +0 -1
- package/dist/src/engine/lunar/metrics.js.map +0 -1
- package/dist/src/engine/lunar/milestones.d.ts.map +0 -1
- package/dist/src/engine/lunar/milestones.js.map +0 -1
- package/dist/src/engine/lunar/names.d.ts.map +0 -1
- package/dist/src/engine/lunar/names.js.map +0 -1
- package/dist/src/engine/lunar/politics.d.ts.map +0 -1
- package/dist/src/engine/lunar/politics.js.map +0 -1
- package/dist/src/engine/lunar/presets.d.ts.map +0 -1
- package/dist/src/engine/lunar/presets.js.map +0 -1
- package/dist/src/engine/lunar/progression-hooks.d.ts.map +0 -1
- package/dist/src/engine/lunar/progression-hooks.js.map +0 -1
- package/dist/src/engine/lunar/prompts.d.ts.map +0 -1
- package/dist/src/engine/lunar/prompts.js.map +0 -1
- package/dist/src/engine/lunar/reactions.d.ts.map +0 -1
- package/dist/src/engine/lunar/reactions.js.map +0 -1
- package/dist/src/engine/lunar/research-bundle.d.ts.map +0 -1
- package/dist/src/engine/lunar/research-bundle.js.map +0 -1
- package/dist/src/engine/mars/effects.d.ts.map +0 -1
- package/dist/src/engine/mars/effects.js.map +0 -1
- package/dist/src/engine/mars/events.d.ts.map +0 -1
- package/dist/src/engine/mars/events.js.map +0 -1
- package/dist/src/engine/mars/fingerprint.d.ts.map +0 -1
- package/dist/src/engine/mars/fingerprint.js.map +0 -1
- package/dist/src/engine/mars/hooks.d.ts.map +0 -1
- package/dist/src/engine/mars/hooks.js.map +0 -1
- package/dist/src/engine/mars/index.d.ts.map +0 -1
- package/dist/src/engine/mars/index.js.map +0 -1
- package/dist/src/engine/mars/metrics.d.ts.map +0 -1
- package/dist/src/engine/mars/metrics.js.map +0 -1
- package/dist/src/engine/mars/milestones.d.ts.map +0 -1
- package/dist/src/engine/mars/milestones.js.map +0 -1
- package/dist/src/engine/mars/names.d.ts.map +0 -1
- package/dist/src/engine/mars/names.js.map +0 -1
- package/dist/src/engine/mars/politics.d.ts.map +0 -1
- package/dist/src/engine/mars/politics.js.map +0 -1
- package/dist/src/engine/mars/presets.d.ts.map +0 -1
- package/dist/src/engine/mars/presets.js.map +0 -1
- package/dist/src/engine/mars/progression-hooks.d.ts.map +0 -1
- package/dist/src/engine/mars/progression-hooks.js.map +0 -1
- package/dist/src/engine/mars/prompts.d.ts.map +0 -1
- package/dist/src/engine/mars/prompts.js.map +0 -1
- package/dist/src/engine/mars/reactions.d.ts.map +0 -1
- package/dist/src/engine/mars/reactions.js.map +0 -1
- package/dist/src/engine/mars/research-bundle.d.ts.map +0 -1
- package/dist/src/engine/mars/research-bundle.js.map +0 -1
- package/dist/src/engine/metric-registry.d.ts.map +0 -1
- package/dist/src/engine/metric-registry.js.map +0 -1
- package/dist/src/engine/provider-resolver.d.ts.map +0 -1
- package/dist/src/engine/provider-resolver.js.map +0 -1
- package/dist/src/engine/types.d.ts.map +0 -1
- package/dist/src/runtime/agent-memory.d.ts.map +0 -1
- package/dist/src/runtime/agent-memory.js.map +0 -1
- package/dist/src/runtime/agent-reactions.d.ts.map +0 -1
- package/dist/src/runtime/agent-reactions.js.map +0 -1
- package/dist/src/runtime/batch.d.ts.map +0 -1
- package/dist/src/runtime/batch.js.map +0 -1
- package/dist/src/runtime/chat-agents.d.ts.map +0 -1
- package/dist/src/runtime/chat-agents.js.map +0 -1
- package/dist/src/runtime/citations-catalog.d.ts.map +0 -1
- package/dist/src/runtime/citations-catalog.js.map +0 -1
- package/dist/src/runtime/commander-setup.d.ts.map +0 -1
- package/dist/src/runtime/commander-setup.js.map +0 -1
- package/dist/src/runtime/contracts.d.ts.map +0 -1
- package/dist/src/runtime/cost-tracker.d.ts.map +0 -1
- package/dist/src/runtime/cost-tracker.js.map +0 -1
- package/dist/src/runtime/departments.d.ts.map +0 -1
- package/dist/src/runtime/departments.js.map +0 -1
- package/dist/src/runtime/director.d.ts.map +0 -1
- package/dist/src/runtime/director.js.map +0 -1
- package/dist/src/runtime/economics-profile.d.ts.map +0 -1
- package/dist/src/runtime/economics-profile.js.map +0 -1
- package/dist/src/runtime/emergent-setup.d.ts.map +0 -1
- package/dist/src/runtime/emergent-setup.js.map +0 -1
- package/dist/src/runtime/generic-fingerprint.d.ts.map +0 -1
- package/dist/src/runtime/generic-fingerprint.js.map +0 -1
- package/dist/src/runtime/hexaco-cues/trajectory.d.ts.map +0 -1
- package/dist/src/runtime/hexaco-cues/trajectory.js.map +0 -1
- package/dist/src/runtime/hexaco-cues/translation.d.ts.map +0 -1
- package/dist/src/runtime/hexaco-cues/translation.js.map +0 -1
- package/dist/src/runtime/index.d.ts.map +0 -1
- package/dist/src/runtime/index.js.map +0 -1
- package/dist/src/runtime/llm-invocations/generateValidatedObject.d.ts.map +0 -1
- package/dist/src/runtime/llm-invocations/generateValidatedObject.js.map +0 -1
- package/dist/src/runtime/llm-invocations/sendAndValidate.d.ts.map +0 -1
- package/dist/src/runtime/llm-invocations/sendAndValidate.js.map +0 -1
- package/dist/src/runtime/orchestrator.d.ts.map +0 -1
- package/dist/src/runtime/orchestrator.js.map +0 -1
- package/dist/src/runtime/output-writer.d.ts.map +0 -1
- package/dist/src/runtime/output-writer.js.map +0 -1
- package/dist/src/runtime/parsers.d.ts.map +0 -1
- package/dist/src/runtime/parsers.js.map +0 -1
- package/dist/src/runtime/pricing.d.ts.map +0 -1
- package/dist/src/runtime/pricing.js.map +0 -1
- package/dist/src/runtime/provider-errors.d.ts.map +0 -1
- package/dist/src/runtime/provider-errors.js.map +0 -1
- package/dist/src/runtime/reaction-step.d.ts.map +0 -1
- package/dist/src/runtime/reaction-step.js.map +0 -1
- package/dist/src/runtime/research/knowledge-base.d.ts.map +0 -1
- package/dist/src/runtime/research/knowledge-base.js.map +0 -1
- package/dist/src/runtime/research/research-memory.d.ts.map +0 -1
- package/dist/src/runtime/research/research-memory.js.map +0 -1
- package/dist/src/runtime/research/research.d.ts.map +0 -1
- package/dist/src/runtime/research/research.js.map +0 -1
- package/dist/src/runtime/research/scenario-research.d.ts.map +0 -1
- package/dist/src/runtime/research/scenario-research.js.map +0 -1
- package/dist/src/runtime/research/scenarios.d.ts.map +0 -1
- package/dist/src/runtime/research/scenarios.js.map +0 -1
- package/dist/src/runtime/runtime-helpers.d.ts.map +0 -1
- package/dist/src/runtime/runtime-helpers.js.map +0 -1
- package/dist/src/runtime/schemas/commander.d.ts.map +0 -1
- package/dist/src/runtime/schemas/commander.js.map +0 -1
- package/dist/src/runtime/schemas/department.d.ts.map +0 -1
- package/dist/src/runtime/schemas/department.js.map +0 -1
- package/dist/src/runtime/schemas/director.d.ts.map +0 -1
- package/dist/src/runtime/schemas/director.js.map +0 -1
- package/dist/src/runtime/schemas/index.d.ts.map +0 -1
- package/dist/src/runtime/schemas/index.js.map +0 -1
- package/dist/src/runtime/schemas/reactions.d.ts.map +0 -1
- package/dist/src/runtime/schemas/reactions.js.map +0 -1
- package/dist/src/runtime/schemas/verdict.d.ts.map +0 -1
- package/dist/src/runtime/schemas/verdict.js.map +0 -1
- package/dist/src/runtime/tool-ledger.d.ts.map +0 -1
- package/dist/src/runtime/tool-ledger.js.map +0 -1
- /package/dist/{src/cli → cli}/sim-config.d.ts +0 -0
- /package/dist/{src/cli → cli}/sim-config.js +0 -0
- /package/dist/{src/cli → cli}/types.d.ts +0 -0
- /package/dist/{src/cli → cli}/types.js +0 -0
- /package/dist/{src/engine → engine}/_template/index.d.ts +0 -0
- /package/dist/{src/engine → engine}/_template/index.js +0 -0
- /package/dist/{src/engine → engine}/compiler/cache.d.ts +0 -0
- /package/dist/{src/engine → engine}/compiler/cache.js +0 -0
- /package/dist/{src/engine → engine}/compiler/generate-director.d.ts +0 -0
- /package/dist/{src/engine → engine}/compiler/generate-director.js +0 -0
- /package/dist/{src/engine → engine}/compiler/generate-fingerprint.d.ts +0 -0
- /package/dist/{src/engine → engine}/compiler/generate-fingerprint.js +0 -0
- /package/dist/{src/engine → engine}/compiler/generate-milestones.d.ts +0 -0
- /package/dist/{src/engine → engine}/compiler/generate-milestones.js +0 -0
- /package/dist/{src/engine → engine}/compiler/generate-politics.d.ts +0 -0
- /package/dist/{src/engine → engine}/compiler/generate-politics.js +0 -0
- /package/dist/{src/engine → engine}/compiler/generate-progression.d.ts +0 -0
- /package/dist/{src/engine → engine}/compiler/generate-progression.js +0 -0
- /package/dist/{src/engine → engine}/compiler/generate-prompts.d.ts +0 -0
- /package/dist/{src/engine → engine}/compiler/generate-prompts.js +0 -0
- /package/dist/{src/engine → engine}/compiler/generate-reactions.d.ts +0 -0
- /package/dist/{src/engine → engine}/compiler/generate-reactions.js +0 -0
- /package/dist/{src/engine → engine}/compiler/index.d.ts +0 -0
- /package/dist/{src/engine → engine}/compiler/index.js +0 -0
- /package/dist/{src/engine → engine}/compiler/llm-invocations/generateValidatedCode.d.ts +0 -0
- /package/dist/{src/engine → engine}/compiler/llm-invocations/generateValidatedCode.js +0 -0
- /package/dist/{src/engine → engine}/compiler/llm-invocations/generateValidatedJson.d.ts +0 -0
- /package/dist/{src/engine → engine}/compiler/llm-invocations/generateValidatedJson.js +0 -0
- /package/dist/{src/engine → engine}/compiler/llm-invocations/generateValidatedObject.d.ts +0 -0
- /package/dist/{src/engine → engine}/compiler/llm-invocations/generateValidatedObject.js +0 -0
- /package/dist/{src/engine → engine}/compiler/llm-invocations/generateValidatedProse.d.ts +0 -0
- /package/dist/{src/engine → engine}/compiler/llm-invocations/generateValidatedProse.js +0 -0
- /package/dist/{src/engine → engine}/compiler/schemas/index.d.ts +0 -0
- /package/dist/{src/engine → engine}/compiler/schemas/index.js +0 -0
- /package/dist/{src/engine → engine}/compiler/schemas/milestones.d.ts +0 -0
- /package/dist/{src/engine → engine}/compiler/schemas/milestones.js +0 -0
- /package/dist/{src/engine → engine}/compiler/seed-ingestion.d.ts +0 -0
- /package/dist/{src/engine → engine}/compiler/seed-ingestion.js +0 -0
- /package/dist/{src/engine → engine}/compiler/telemetry.d.ts +0 -0
- /package/dist/{src/engine → engine}/compiler/telemetry.js +0 -0
- /package/dist/{src/engine → engine}/compiler/types.d.ts +0 -0
- /package/dist/{src/engine → engine}/compiler/types.js +0 -0
- /package/dist/{src/engine → engine}/compiler/validate.d.ts +0 -0
- /package/dist/{src/engine → engine}/compiler/validate.js +0 -0
- /package/dist/{src/engine → engine}/core/agent-generator.d.ts +0 -0
- /package/dist/{src/engine → engine}/core/agent-generator.js +0 -0
- /package/dist/{src/engine → engine}/core/kernel.d.ts +0 -0
- /package/dist/{src/engine → engine}/core/kernel.js +0 -0
- /package/dist/{src/engine → engine}/core/progression.d.ts +0 -0
- /package/dist/{src/engine → engine}/core/progression.js +0 -0
- /package/dist/{src/engine → engine}/core/rng.d.ts +0 -0
- /package/dist/{src/engine → engine}/core/rng.js +0 -0
- /package/dist/{src/engine → engine}/core/state.d.ts +0 -0
- /package/dist/{src/engine → engine}/core/state.js +0 -0
- /package/dist/{src/engine → engine}/effect-registry.d.ts +0 -0
- /package/dist/{src/engine → engine}/effect-registry.js +0 -0
- /package/dist/{src/engine → engine}/event-taxonomy.d.ts +0 -0
- /package/dist/{src/engine → engine}/event-taxonomy.js +0 -0
- /package/dist/{src/engine → engine}/lunar/effects.d.ts +0 -0
- /package/dist/{src/engine → engine}/lunar/effects.js +0 -0
- /package/dist/{src/engine → engine}/lunar/events.d.ts +0 -0
- /package/dist/{src/engine → engine}/lunar/events.js +0 -0
- /package/dist/{src/engine → engine}/lunar/fingerprint.d.ts +0 -0
- /package/dist/{src/engine → engine}/lunar/fingerprint.js +0 -0
- /package/dist/{src/engine → engine}/lunar/hooks.d.ts +0 -0
- /package/dist/{src/engine → engine}/lunar/hooks.js +0 -0
- /package/dist/{src/engine → engine}/lunar/index.d.ts +0 -0
- /package/dist/{src/engine → engine}/lunar/index.js +0 -0
- /package/dist/{src/engine → engine}/lunar/metrics.d.ts +0 -0
- /package/dist/{src/engine → engine}/lunar/metrics.js +0 -0
- /package/dist/{src/engine → engine}/lunar/milestones.d.ts +0 -0
- /package/dist/{src/engine → engine}/lunar/milestones.js +0 -0
- /package/dist/{src/engine → engine}/lunar/names.d.ts +0 -0
- /package/dist/{src/engine → engine}/lunar/names.js +0 -0
- /package/dist/{src/engine → engine}/lunar/politics.d.ts +0 -0
- /package/dist/{src/engine → engine}/lunar/politics.js +0 -0
- /package/dist/{src/engine → engine}/lunar/presets.d.ts +0 -0
- /package/dist/{src/engine → engine}/lunar/presets.js +0 -0
- /package/dist/{src/engine → engine}/lunar/progression-hooks.d.ts +0 -0
- /package/dist/{src/engine → engine}/lunar/progression-hooks.js +0 -0
- /package/dist/{src/engine → engine}/lunar/prompts.d.ts +0 -0
- /package/dist/{src/engine → engine}/lunar/prompts.js +0 -0
- /package/dist/{src/engine → engine}/lunar/reactions.d.ts +0 -0
- /package/dist/{src/engine → engine}/lunar/reactions.js +0 -0
- /package/dist/{src/engine → engine}/lunar/research-bundle.d.ts +0 -0
- /package/dist/{src/engine → engine}/lunar/research-bundle.js +0 -0
- /package/dist/{src/engine → engine}/lunar/scenario.json +0 -0
- /package/dist/{src/engine → engine}/mars/effects.d.ts +0 -0
- /package/dist/{src/engine → engine}/mars/effects.js +0 -0
- /package/dist/{src/engine → engine}/mars/events.d.ts +0 -0
- /package/dist/{src/engine → engine}/mars/events.js +0 -0
- /package/dist/{src/engine → engine}/mars/fingerprint.d.ts +0 -0
- /package/dist/{src/engine → engine}/mars/fingerprint.js +0 -0
- /package/dist/{src/engine → engine}/mars/hooks.d.ts +0 -0
- /package/dist/{src/engine → engine}/mars/hooks.js +0 -0
- /package/dist/{src/engine → engine}/mars/index.d.ts +0 -0
- /package/dist/{src/engine → engine}/mars/index.js +0 -0
- /package/dist/{src/engine → engine}/mars/metrics.d.ts +0 -0
- /package/dist/{src/engine → engine}/mars/metrics.js +0 -0
- /package/dist/{src/engine → engine}/mars/milestones.d.ts +0 -0
- /package/dist/{src/engine → engine}/mars/milestones.js +0 -0
- /package/dist/{src/engine → engine}/mars/names.d.ts +0 -0
- /package/dist/{src/engine → engine}/mars/names.js +0 -0
- /package/dist/{src/engine → engine}/mars/politics.d.ts +0 -0
- /package/dist/{src/engine → engine}/mars/politics.js +0 -0
- /package/dist/{src/engine → engine}/mars/presets.d.ts +0 -0
- /package/dist/{src/engine → engine}/mars/presets.js +0 -0
- /package/dist/{src/engine → engine}/mars/progression-hooks.d.ts +0 -0
- /package/dist/{src/engine → engine}/mars/progression-hooks.js +0 -0
- /package/dist/{src/engine → engine}/mars/prompts.d.ts +0 -0
- /package/dist/{src/engine → engine}/mars/prompts.js +0 -0
- /package/dist/{src/engine → engine}/mars/reactions.d.ts +0 -0
- /package/dist/{src/engine → engine}/mars/reactions.js +0 -0
- /package/dist/{src/engine → engine}/mars/research-bundle.d.ts +0 -0
- /package/dist/{src/engine → engine}/mars/research-bundle.js +0 -0
- /package/dist/{src/engine → engine}/mars/scenario.json +0 -0
- /package/dist/{src/engine → engine}/metric-registry.d.ts +0 -0
- /package/dist/{src/engine → engine}/metric-registry.js +0 -0
- /package/dist/{src/engine → engine}/provider-resolver.d.ts +0 -0
- /package/dist/{src/engine → engine}/types.d.ts +0 -0
- /package/dist/{src/engine → engine}/types.js +0 -0
- /package/dist/{src/runtime → runtime}/agent-memory.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/agent-memory.js +0 -0
- /package/dist/{src/runtime → runtime}/agent-reactions.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/agent-reactions.js +0 -0
- /package/dist/{src/runtime → runtime}/batch.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/batch.js +0 -0
- /package/dist/{src/runtime → runtime}/chat-agents.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/chat-agents.js +0 -0
- /package/dist/{src/runtime → runtime}/citations-catalog.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/citations-catalog.js +0 -0
- /package/dist/{src/runtime → runtime}/commander-setup.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/commander-setup.js +0 -0
- /package/dist/{src/runtime → runtime}/contracts.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/contracts.js +0 -0
- /package/dist/{src/runtime → runtime}/cost-tracker.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/cost-tracker.js +0 -0
- /package/dist/{src/runtime → runtime}/departments.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/departments.js +0 -0
- /package/dist/{src/runtime → runtime}/director.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/director.js +0 -0
- /package/dist/{src/runtime → runtime}/economics-profile.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/economics-profile.js +0 -0
- /package/dist/{src/runtime → runtime}/emergent-setup.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/emergent-setup.js +0 -0
- /package/dist/{src/runtime → runtime}/generic-fingerprint.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/generic-fingerprint.js +0 -0
- /package/dist/{src/runtime → runtime}/hexaco-cues/trajectory.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/hexaco-cues/trajectory.js +0 -0
- /package/dist/{src/runtime → runtime}/hexaco-cues/translation.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/hexaco-cues/translation.js +0 -0
- /package/dist/{src/runtime → runtime}/index.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/index.js +0 -0
- /package/dist/{src/runtime → runtime}/llm-invocations/generateValidatedObject.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/llm-invocations/generateValidatedObject.js +0 -0
- /package/dist/{src/runtime → runtime}/llm-invocations/sendAndValidate.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/llm-invocations/sendAndValidate.js +0 -0
- /package/dist/{src/runtime → runtime}/orchestrator.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/orchestrator.js +0 -0
- /package/dist/{src/runtime → runtime}/output-writer.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/output-writer.js +0 -0
- /package/dist/{src/runtime → runtime}/parsers.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/parsers.js +0 -0
- /package/dist/{src/runtime → runtime}/pricing.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/pricing.js +0 -0
- /package/dist/{src/runtime → runtime}/provider-errors.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/provider-errors.js +0 -0
- /package/dist/{src/runtime → runtime}/reaction-step.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/reaction-step.js +0 -0
- /package/dist/{src/runtime → runtime}/research/knowledge-base.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/research/knowledge-base.js +0 -0
- /package/dist/{src/runtime → runtime}/research/research-memory.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/research/research-memory.js +0 -0
- /package/dist/{src/runtime → runtime}/research/research.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/research/research.js +0 -0
- /package/dist/{src/runtime → runtime}/research/scenario-research.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/research/scenario-research.js +0 -0
- /package/dist/{src/runtime → runtime}/research/scenarios.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/research/scenarios.js +0 -0
- /package/dist/{src/runtime → runtime}/runtime-helpers.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/runtime-helpers.js +0 -0
- /package/dist/{src/runtime → runtime}/schemas/commander.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/schemas/commander.js +0 -0
- /package/dist/{src/runtime → runtime}/schemas/department.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/schemas/department.js +0 -0
- /package/dist/{src/runtime → runtime}/schemas/director.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/schemas/director.js +0 -0
- /package/dist/{src/runtime → runtime}/schemas/index.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/schemas/index.js +0 -0
- /package/dist/{src/runtime → runtime}/schemas/reactions.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/schemas/reactions.js +0 -0
- /package/dist/{src/runtime → runtime}/schemas/verdict.d.ts +0 -0
- /package/dist/{src/runtime → runtime}/schemas/verdict.js +0 -0
- /package/dist/{src/runtime → runtime}/tool-ledger.d.ts +0 -0
- /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> · Apache-2.0 · <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
|