paracosm 0.7.409 → 0.7.431
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 +79 -14
- package/dist/cli/compile.d.ts +1 -1
- package/dist/cli/compile.js +4 -4
- package/dist/cli/fetch-seed-url.d.ts +48 -0
- package/dist/cli/fetch-seed-url.d.ts.map +1 -0
- package/dist/cli/fetch-seed-url.js +109 -0
- package/dist/cli/fetch-seed-url.js.map +1 -0
- package/dist/cli/fork-preconditions.d.ts +17 -0
- package/dist/cli/fork-preconditions.d.ts.map +1 -0
- package/dist/cli/fork-preconditions.js +51 -0
- package/dist/cli/fork-preconditions.js.map +1 -0
- package/dist/cli/init-templates.d.ts +43 -0
- package/dist/cli/init-templates.d.ts.map +1 -0
- package/dist/cli/init-templates.js +141 -0
- package/dist/cli/init-templates.js.map +1 -0
- package/dist/cli/init.d.ts +46 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +207 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/pair-runner.d.ts +25 -2
- package/dist/cli/pair-runner.d.ts.map +1 -1
- package/dist/cli/pair-runner.js +141 -5
- package/dist/cli/pair-runner.js.map +1 -1
- package/dist/cli/quickstart-routes.d.ts +35 -0
- package/dist/cli/quickstart-routes.d.ts.map +1 -0
- package/dist/cli/quickstart-routes.js +100 -0
- package/dist/cli/quickstart-routes.js.map +1 -0
- package/dist/cli/run.js +8 -0
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/server/enrich-run-record.d.ts +22 -0
- package/dist/cli/server/enrich-run-record.d.ts.map +1 -0
- package/dist/cli/server/enrich-run-record.js +27 -0
- package/dist/cli/server/enrich-run-record.js.map +1 -0
- package/dist/cli/server/routes/platform-api.d.ts +19 -3
- package/dist/cli/server/routes/platform-api.d.ts.map +1 -1
- package/dist/cli/server/routes/platform-api.js +174 -5
- package/dist/cli/server/routes/platform-api.js.map +1 -1
- package/dist/cli/server/run-history-store.d.ts +27 -1
- package/dist/cli/server/run-history-store.d.ts.map +1 -1
- package/dist/cli/server/run-history-store.js +5 -0
- package/dist/cli/server/run-history-store.js.map +1 -1
- package/dist/cli/server/run-record.d.ts +13 -0
- package/dist/cli/server/run-record.d.ts.map +1 -1
- package/dist/cli/server/run-record.js.map +1 -1
- package/dist/cli/server/sqlite-run-history-store.d.ts +6 -0
- package/dist/cli/server/sqlite-run-history-store.d.ts.map +1 -0
- package/dist/cli/server/sqlite-run-history-store.js +226 -0
- package/dist/cli/server/sqlite-run-history-store.js.map +1 -0
- package/dist/cli/server-app.d.ts +73 -2
- package/dist/cli/server-app.d.ts.map +1 -1
- package/dist/cli/server-app.js +409 -159
- package/dist/cli/server-app.js.map +1 -1
- package/dist/cli/session-title.js +1 -1
- package/dist/cli/sim-config.d.ts +13 -0
- package/dist/cli/sim-config.d.ts.map +1 -1
- package/dist/cli/sim-config.js +8 -1
- package/dist/cli/sim-config.js.map +1 -1
- package/dist/cli/simulate-route.d.ts +103 -0
- package/dist/cli/simulate-route.d.ts.map +1 -0
- package/dist/cli/simulate-route.js +95 -0
- package/dist/cli/simulate-route.js.map +1 -0
- package/dist/engine/compiler/cache.d.ts +2 -2
- package/dist/engine/compiler/cache.js +2 -2
- package/dist/engine/compiler/compile-from-seed.d.ts +78 -0
- package/dist/engine/compiler/compile-from-seed.d.ts.map +1 -0
- package/dist/engine/compiler/compile-from-seed.js +93 -0
- package/dist/engine/compiler/compile-from-seed.js.map +1 -0
- package/dist/engine/compiler/generate-fingerprint.d.ts.map +1 -1
- package/dist/engine/compiler/generate-fingerprint.js +3 -6
- package/dist/engine/compiler/generate-fingerprint.js.map +1 -1
- package/dist/engine/compiler/generate-politics.d.ts.map +1 -1
- package/dist/engine/compiler/generate-politics.js +3 -6
- package/dist/engine/compiler/generate-politics.js.map +1 -1
- package/dist/engine/compiler/generate-progression.d.ts +10 -1
- package/dist/engine/compiler/generate-progression.d.ts.map +1 -1
- package/dist/engine/compiler/generate-progression.js +15 -15
- package/dist/engine/compiler/generate-progression.js.map +1 -1
- package/dist/engine/compiler/generate-prompts.d.ts.map +1 -1
- package/dist/engine/compiler/generate-prompts.js +4 -7
- package/dist/engine/compiler/generate-prompts.js.map +1 -1
- package/dist/engine/compiler/generate-reactions.d.ts.map +1 -1
- package/dist/engine/compiler/generate-reactions.js +3 -6
- package/dist/engine/compiler/generate-reactions.js.map +1 -1
- package/dist/engine/compiler/index.d.ts +9 -6
- package/dist/engine/compiler/index.d.ts.map +1 -1
- package/dist/engine/compiler/index.js +9 -6
- package/dist/engine/compiler/index.js.map +1 -1
- package/dist/engine/compiler/sandbox-runner.d.ts +40 -0
- package/dist/engine/compiler/sandbox-runner.d.ts.map +1 -0
- package/dist/engine/compiler/sandbox-runner.js +278 -0
- package/dist/engine/compiler/sandbox-runner.js.map +1 -0
- package/dist/engine/compiler/scenario-fixture.d.ts +4 -4
- package/dist/engine/compiler/scenario-fixture.js +4 -4
- package/dist/engine/compiler/seed-ingestion.d.ts +2 -2
- package/dist/engine/compiler/seed-ingestion.js +2 -2
- package/dist/engine/compiler/state-shape-block.d.ts +1 -1
- package/dist/engine/compiler/state-shape-block.js +5 -5
- package/dist/engine/compiler/types.d.ts +2 -2
- package/dist/engine/compiler/types.d.ts.map +1 -1
- package/dist/engine/compiler/validate.js +1 -1
- package/dist/engine/core/kernel.d.ts +4 -4
- package/dist/engine/core/kernel.js +6 -6
- package/dist/engine/core/progression.d.ts +3 -3
- package/dist/engine/core/progression.js +21 -21
- package/dist/engine/core/state.d.ts +7 -5
- package/dist/engine/core/state.d.ts.map +1 -1
- package/dist/engine/digital-twin/index.d.ts +41 -0
- package/dist/engine/digital-twin/index.d.ts.map +1 -0
- package/dist/engine/digital-twin/index.js +40 -0
- package/dist/engine/digital-twin/index.js.map +1 -0
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/leader-presets.d.ts +39 -0
- package/dist/engine/leader-presets.d.ts.map +1 -0
- package/dist/engine/leader-presets.js +118 -0
- package/dist/engine/leader-presets.js.map +1 -0
- package/dist/engine/lunar/fingerprint.js +2 -2
- package/dist/engine/lunar/prompts.js +2 -2
- package/dist/engine/mars/fingerprint.js +1 -1
- package/dist/engine/mars/metrics.d.ts +2 -2
- package/dist/engine/mars/metrics.js +2 -2
- package/dist/engine/mars/prompts.js +3 -3
- package/dist/runtime/build-artifact.d.ts +2 -1
- package/dist/runtime/build-artifact.d.ts.map +1 -1
- package/dist/runtime/build-artifact.js +2 -1
- package/dist/runtime/build-artifact.js.map +1 -1
- package/dist/runtime/canonical-json.d.ts +25 -0
- package/dist/runtime/canonical-json.d.ts.map +1 -0
- package/dist/runtime/canonical-json.js +46 -0
- package/dist/runtime/canonical-json.js.map +1 -0
- package/dist/runtime/contracts.d.ts +1 -1
- package/dist/runtime/departments.js +1 -1
- package/dist/runtime/orchestrator.d.ts +44 -4
- package/dist/runtime/orchestrator.d.ts.map +1 -1
- package/dist/runtime/orchestrator.js +125 -33
- package/dist/runtime/orchestrator.js.map +1 -1
- package/dist/runtime/parsers.js +5 -5
- package/dist/runtime/reaction-step.js +2 -2
- package/dist/runtime/world-model/index.d.ts +172 -18
- package/dist/runtime/world-model/index.d.ts.map +1 -1
- package/dist/runtime/world-model/index.js +242 -4
- package/dist/runtime/world-model/index.js.map +1 -1
- package/dist/runtime/world-snapshot.d.ts +13 -0
- package/dist/runtime/world-snapshot.d.ts.map +1 -0
- package/dist/runtime/world-snapshot.js +19 -0
- package/dist/runtime/world-snapshot.js.map +1 -0
- package/package.json +19 -4
package/dist/cli/server-app.js
CHANGED
|
@@ -3,7 +3,10 @@ import { readFileSync, writeFileSync, existsSync, readdirSync, statSync, unlinkS
|
|
|
3
3
|
import { resolve, dirname } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { normalizeSimulationConfig, applyDemoCaps } from './sim-config.js';
|
|
6
|
-
import { runPairSimulations, runForkSimulation } from './pair-runner.js';
|
|
6
|
+
import { runPairSimulations, runForkSimulation, runBatchSimulations } from './pair-runner.js';
|
|
7
|
+
import { handleFetchSeed, handleCompileFromSeed, handleGenerateLeaders, } from './quickstart-routes.js';
|
|
8
|
+
import { handleSimulate } from './simulate-route.js';
|
|
9
|
+
import { compileScenario as compileScenarioReal } from '../engine/compiler/index.js';
|
|
7
10
|
import { marsScenario } from '../engine/mars/index.js';
|
|
8
11
|
import { lunarScenario } from '../engine/lunar/index.js';
|
|
9
12
|
import { describeCustomScenarioSource, isRunnableScenarioPackage, loadDiskCustomScenarios, } from './custom-scenarios.js';
|
|
@@ -14,9 +17,13 @@ import { openSessionStore } from './session-store.js';
|
|
|
14
17
|
import { generateSessionTitle } from './session-title.js';
|
|
15
18
|
import { resolveServerMode } from './server/server-mode.js';
|
|
16
19
|
import { createRunRecord, hashLeaderConfig } from './server/run-record.js';
|
|
20
|
+
import { enrichRunRecordFromArtifact } from './server/enrich-run-record.js';
|
|
17
21
|
import { createNoopRunHistoryStore } from './server/run-history-store.js';
|
|
22
|
+
import { createSqliteRunHistoryStore } from './server/sqlite-run-history-store.js';
|
|
18
23
|
import { handlePublicDemoRoute } from './server/routes/public-demo.js';
|
|
19
24
|
import { handlePlatformApiRoute } from './server/routes/platform-api.js';
|
|
25
|
+
import { validateForkSetupPreconditions } from './fork-preconditions.js';
|
|
26
|
+
import { fetchSeedFromUrl } from './fetch-seed-url.js';
|
|
20
27
|
function projectScenarioForClient(sc) {
|
|
21
28
|
return {
|
|
22
29
|
id: sc.id,
|
|
@@ -58,14 +65,167 @@ const PARACOSM_VERSION = (() => {
|
|
|
58
65
|
return 'unknown';
|
|
59
66
|
}
|
|
60
67
|
})();
|
|
61
|
-
|
|
68
|
+
class RequestBodyTooLargeError extends Error {
|
|
69
|
+
statusCode = 413;
|
|
70
|
+
constructor(maxBytes) {
|
|
71
|
+
super(`Request body too large. Maximum ${maxBytes} bytes.`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function writeJsonError(res, error, fallbackStatus = 400) {
|
|
75
|
+
const status = error instanceof RequestBodyTooLargeError
|
|
76
|
+
? error.statusCode
|
|
77
|
+
: fallbackStatus;
|
|
78
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
79
|
+
res.end(JSON.stringify({ error: String(error) }));
|
|
80
|
+
}
|
|
81
|
+
function readBody(req, maxBytes) {
|
|
62
82
|
return new Promise((resolveBody, reject) => {
|
|
63
83
|
let body = '';
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
84
|
+
let receivedBytes = 0;
|
|
85
|
+
let settled = false;
|
|
86
|
+
let tooLarge = false;
|
|
87
|
+
const fail = (error) => {
|
|
88
|
+
if (settled)
|
|
89
|
+
return;
|
|
90
|
+
settled = true;
|
|
91
|
+
reject(error);
|
|
92
|
+
};
|
|
93
|
+
req.on('data', chunk => {
|
|
94
|
+
if (tooLarge)
|
|
95
|
+
return;
|
|
96
|
+
receivedBytes += Buffer.isBuffer(chunk)
|
|
97
|
+
? chunk.length
|
|
98
|
+
: Buffer.byteLength(String(chunk));
|
|
99
|
+
if (receivedBytes > maxBytes) {
|
|
100
|
+
tooLarge = true;
|
|
101
|
+
body = '';
|
|
102
|
+
fail(new RequestBodyTooLargeError(maxBytes));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
body += chunk;
|
|
106
|
+
});
|
|
107
|
+
req.on('end', () => {
|
|
108
|
+
if (settled)
|
|
109
|
+
return;
|
|
110
|
+
settled = true;
|
|
111
|
+
resolveBody(body);
|
|
112
|
+
});
|
|
113
|
+
req.on('error', fail);
|
|
67
114
|
});
|
|
68
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Resolve the production run-history store. SQLite by default at
|
|
118
|
+
* `${APP_DIR}/data/runs.db`; env override `PARACOSM_RUN_HISTORY_DB_PATH`.
|
|
119
|
+
* Set `PARACOSM_DISABLE_RUN_HISTORY=1` to fall back to the noop store
|
|
120
|
+
* (useful for ephemeral test environments and CLI smoke tests).
|
|
121
|
+
*/
|
|
122
|
+
function resolveRunHistoryStore(env) {
|
|
123
|
+
if (env.PARACOSM_DISABLE_RUN_HISTORY === '1') {
|
|
124
|
+
return createNoopRunHistoryStore();
|
|
125
|
+
}
|
|
126
|
+
const dbPath = env.PARACOSM_RUN_HISTORY_DB_PATH
|
|
127
|
+
?? resolve(env.APP_DIR || '.', 'data', 'runs.db');
|
|
128
|
+
return createSqliteRunHistoryStore({ dbPath });
|
|
129
|
+
}
|
|
130
|
+
export function buildResultsPayloadFromEventBuffer(eventBuffer) {
|
|
131
|
+
const simEvents = eventBuffer
|
|
132
|
+
.filter(msg => msg.startsWith('event: sim\n') || msg.startsWith('event: result\n') || msg.startsWith('event: verdict\n') || msg.startsWith('event: complete\n'))
|
|
133
|
+
.map(msg => {
|
|
134
|
+
const lines = msg.split('\n');
|
|
135
|
+
const eventType = lines[0]?.replace('event: ', '') || '';
|
|
136
|
+
try {
|
|
137
|
+
return { event: eventType, data: JSON.parse(lines[1]?.replace('data: ', '') || '{}') };
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return { event: eventType, data: {} };
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
const results = simEvents.filter(e => e.event === 'result').map(e => e.data);
|
|
144
|
+
const verdict = simEvents.find(e => e.event === 'verdict')?.data || null;
|
|
145
|
+
const isComplete = simEvents.some(e => e.event === 'complete');
|
|
146
|
+
const turns = simEvents.filter(e => e.event === 'sim' && e.data?.type === 'turn_start').length / 2;
|
|
147
|
+
// Reconstruct per-leader timelines from the sim event stream.
|
|
148
|
+
// Group every sim event by leader name, then bucket interesting
|
|
149
|
+
// payload types into typed lists so consumers can pull turn-by-turn
|
|
150
|
+
// crisis info, dept reports, decisions, forges, citations, reactions.
|
|
151
|
+
const byLeader = new Map();
|
|
152
|
+
const ensureLeader = (name) => {
|
|
153
|
+
if (!byLeader.has(name))
|
|
154
|
+
byLeader.set(name, { events: [], decisions: [], forges: [], citations: [], deptReports: [], agentReactions: [], promotions: [], systemsSnapshots: [] });
|
|
155
|
+
return byLeader.get(name);
|
|
156
|
+
};
|
|
157
|
+
// Track decision pending state so we can attach it to outcomes per event.
|
|
158
|
+
const pendingDecision = new Map();
|
|
159
|
+
for (const e of simEvents) {
|
|
160
|
+
if (e.event !== 'sim')
|
|
161
|
+
continue;
|
|
162
|
+
const inner = e.data;
|
|
163
|
+
const type = String(inner.type || '');
|
|
164
|
+
const leader = String(inner.leader || '');
|
|
165
|
+
if (!leader)
|
|
166
|
+
continue;
|
|
167
|
+
const slot = ensureLeader(leader);
|
|
168
|
+
const data = inner.data ?? {};
|
|
169
|
+
const turn = data.turn;
|
|
170
|
+
const time = data.time;
|
|
171
|
+
const eventIndex = data.eventIndex;
|
|
172
|
+
const pendKey = `${leader}-${turn}-${eventIndex ?? 0}`;
|
|
173
|
+
if (type === 'event_start') {
|
|
174
|
+
slot.events.push({ turn, time, eventIndex, title: data.title, category: data.category, description: data.description, emergent: data.emergent });
|
|
175
|
+
}
|
|
176
|
+
else if (type === 'turn_start' && data.title && data.title !== 'Director generating...') {
|
|
177
|
+
slot.events.push({ turn, time, title: data.title, category: data.category, description: data.crisis, emergent: data.emergent });
|
|
178
|
+
}
|
|
179
|
+
else if (type === 'decision_made') {
|
|
180
|
+
pendingDecision.set(pendKey, {
|
|
181
|
+
decision: data.decision,
|
|
182
|
+
rationale: data.rationale,
|
|
183
|
+
selectedPolicies: data.selectedPolicies,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
else if (type === 'outcome') {
|
|
187
|
+
const p = pendingDecision.get(pendKey);
|
|
188
|
+
slot.decisions.push({ turn, time, eventIndex, ...p, outcome: data.outcome });
|
|
189
|
+
pendingDecision.delete(pendKey);
|
|
190
|
+
}
|
|
191
|
+
else if (type === 'specialist_done') {
|
|
192
|
+
const dept = data.department;
|
|
193
|
+
const cites = data.citationList || [];
|
|
194
|
+
slot.deptReports.push({
|
|
195
|
+
turn, time, eventIndex, department: dept,
|
|
196
|
+
summary: data.summary,
|
|
197
|
+
risks: data.risks,
|
|
198
|
+
recommendedActions: data.recommendedActions,
|
|
199
|
+
citations: cites.length,
|
|
200
|
+
toolCount: Array.isArray(data.forgedTools) ? data.forgedTools.length : 0,
|
|
201
|
+
});
|
|
202
|
+
for (const c of cites) {
|
|
203
|
+
slot.citations.push({ ...c, department: dept, turn });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else if (type === 'forge_attempt') {
|
|
207
|
+
slot.forges.push({ turn, time, eventIndex, ...data });
|
|
208
|
+
}
|
|
209
|
+
else if (type === 'agent_reactions') {
|
|
210
|
+
slot.agentReactions.push({ turn, time, reactions: data.reactions, totalReactions: data.totalReactions });
|
|
211
|
+
}
|
|
212
|
+
else if (type === 'promotion') {
|
|
213
|
+
slot.promotions.push({ ...data });
|
|
214
|
+
}
|
|
215
|
+
else if (type === 'systems_snapshot') {
|
|
216
|
+
slot.systemsSnapshots.push({ turn, time, ...data });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const leaders = [...byLeader.entries()].map(([name, slot]) => ({ name, ...slot }));
|
|
220
|
+
return {
|
|
221
|
+
results,
|
|
222
|
+
verdict,
|
|
223
|
+
isComplete,
|
|
224
|
+
turnsCompleted: Math.floor(turns),
|
|
225
|
+
totalEvents: simEvents.length,
|
|
226
|
+
leaders,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
69
229
|
export function createMarsServer(options = {}) {
|
|
70
230
|
const env = options.env ?? process.env;
|
|
71
231
|
const serverMode = resolveServerMode(env);
|
|
@@ -77,6 +237,11 @@ export function createMarsServer(options = {}) {
|
|
|
77
237
|
// env var or maxSimsPerDay option when hosting on your own infra.
|
|
78
238
|
const maxSims = options.maxSimsPerDay ?? parseInt(env.RATE_LIMIT || '1', 10);
|
|
79
239
|
const adminWrite = (env.ADMIN_WRITE || 'false').toLowerCase() === 'true';
|
|
240
|
+
const parsedMaxRequestBodyBytes = Number.parseInt(env.MAX_REQUEST_BODY_BYTES || '', 10);
|
|
241
|
+
const defaultMaxRequestBodyBytes = Number.isFinite(parsedMaxRequestBodyBytes)
|
|
242
|
+
? parsedMaxRequestBodyBytes
|
|
243
|
+
: 5 * 1024 * 1024;
|
|
244
|
+
const maxRequestBodyBytes = options.maxRequestBodyBytes ?? defaultMaxRequestBodyBytes;
|
|
80
245
|
const scenarioDir = options.scenarioDir ?? resolve(__dirname, '..', '..', 'scenarios');
|
|
81
246
|
// Rate-limit state survives pm2 restarts via a JSON file alongside
|
|
82
247
|
// the repo. Without this, a restart gives every blocked IP a full
|
|
@@ -197,7 +362,7 @@ export function createMarsServer(options = {}) {
|
|
|
197
362
|
// routes just return 503.
|
|
198
363
|
console.log(` [sessions] Failed to open session store: ${err}`);
|
|
199
364
|
}
|
|
200
|
-
const runHistoryStore = options.runHistoryStore ??
|
|
365
|
+
const runHistoryStore = options.runHistoryStore ?? resolveRunHistoryStore(env);
|
|
201
366
|
// Coalesce disk writes so a burst of broadcasts (e.g. 50 forge_attempt
|
|
202
367
|
// events during a turn) only triggers one persist call. 500ms debounce
|
|
203
368
|
// is short enough that a crash loses at most a half-second of events
|
|
@@ -563,7 +728,17 @@ export function createMarsServer(options = {}) {
|
|
|
563
728
|
if (handlePublicDemoRoute(serverMode, req, res, corsHeaders)) {
|
|
564
729
|
return;
|
|
565
730
|
}
|
|
566
|
-
|
|
731
|
+
const runHistoryRoutesDefault = serverMode !== 'hosted_demo';
|
|
732
|
+
const runHistoryEnvFlag = (env.PARACOSM_ENABLE_RUN_HISTORY_ROUTES ?? '').toLowerCase();
|
|
733
|
+
const paracosmRoutesEnabled = runHistoryEnvFlag === 'true' ? true :
|
|
734
|
+
runHistoryEnvFlag === 'false' ? false :
|
|
735
|
+
runHistoryRoutesDefault;
|
|
736
|
+
if (await handlePlatformApiRoute(req, res, {
|
|
737
|
+
runHistoryStore,
|
|
738
|
+
corsHeaders,
|
|
739
|
+
paracosmRoutesEnabled,
|
|
740
|
+
scenarioLookup: (id) => customScenarioCatalog.get(id)?.scenario,
|
|
741
|
+
})) {
|
|
567
742
|
return;
|
|
568
743
|
}
|
|
569
744
|
if (req.url === '/events') {
|
|
@@ -681,7 +856,7 @@ export function createMarsServer(options = {}) {
|
|
|
681
856
|
// Store a scenario in memory (always allowed) or save to disk (requires ADMIN_WRITE)
|
|
682
857
|
if (req.url === '/scenario/store' && req.method === 'POST') {
|
|
683
858
|
try {
|
|
684
|
-
const { scenario: scenarioJson, saveToDisk } = JSON.parse(await readBody(req));
|
|
859
|
+
const { scenario: scenarioJson, saveToDisk } = JSON.parse(await readBody(req, maxRequestBodyBytes));
|
|
685
860
|
if (!scenarioJson || !scenarioJson.id) {
|
|
686
861
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
687
862
|
res.end(JSON.stringify({ error: 'scenario with id required' }));
|
|
@@ -714,8 +889,7 @@ export function createMarsServer(options = {}) {
|
|
|
714
889
|
res.end(JSON.stringify({ stored: true, id: scenarioJson.id, savedToDisk, adminWrite, switchable }));
|
|
715
890
|
}
|
|
716
891
|
catch (err) {
|
|
717
|
-
res
|
|
718
|
-
res.end(JSON.stringify({ error: String(err) }));
|
|
892
|
+
writeJsonError(res, err);
|
|
719
893
|
}
|
|
720
894
|
return;
|
|
721
895
|
}
|
|
@@ -726,35 +900,166 @@ export function createMarsServer(options = {}) {
|
|
|
726
900
|
// were /scenario/store'd but never compiled get a specific
|
|
727
901
|
// "needs compile" error instead of the misleading "Unknown".
|
|
728
902
|
if (req.url === '/scenario/switch' && req.method === 'POST') {
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
903
|
+
try {
|
|
904
|
+
const { id } = JSON.parse(await readBody(req, maxRequestBodyBytes));
|
|
905
|
+
const entry = customScenarioCatalog.get(id);
|
|
906
|
+
if (entry) {
|
|
907
|
+
activeScenario = entry.scenario;
|
|
908
|
+
}
|
|
909
|
+
else if (memoryScenarios.has(id)) {
|
|
910
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
911
|
+
res.end(JSON.stringify({
|
|
912
|
+
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.`,
|
|
913
|
+
storedButUnrunnable: true,
|
|
914
|
+
id,
|
|
915
|
+
}));
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
else {
|
|
919
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
920
|
+
res.end(JSON.stringify({ error: `Unknown scenario: ${id}. Use /compile or /scenario/store for custom scenarios.` }));
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
clearEventBuffer();
|
|
924
|
+
simConfig = null;
|
|
925
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
926
|
+
res.end(JSON.stringify({ active: activeScenario.id, name: activeScenario.labels?.name }));
|
|
733
927
|
}
|
|
734
|
-
|
|
735
|
-
res
|
|
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;
|
|
928
|
+
catch (err) {
|
|
929
|
+
writeJsonError(res, err);
|
|
742
930
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
// Tier 5 Quickstart onboarding endpoints.
|
|
934
|
+
if (req.url?.startsWith('/api/quickstart/') && req.method === 'POST') {
|
|
935
|
+
try {
|
|
936
|
+
const body = JSON.parse(await readBody(req, maxRequestBodyBytes));
|
|
937
|
+
const quickstartDeps = {
|
|
938
|
+
setActiveScenario: (sc) => {
|
|
939
|
+
activeScenario = sc;
|
|
940
|
+
// Quickstart-compiled scenarios reuse the 'compiled' source
|
|
941
|
+
// tag; same post-compile semantics, no new source state
|
|
942
|
+
// required.
|
|
943
|
+
customScenarioCatalog.set(sc.id, { scenario: sc, source: 'compiled' });
|
|
944
|
+
},
|
|
945
|
+
getScenarioById: (id) => {
|
|
946
|
+
if (id === activeScenario.id)
|
|
947
|
+
return activeScenario;
|
|
948
|
+
return customScenarioCatalog.get(id)?.scenario;
|
|
949
|
+
},
|
|
950
|
+
fetchSeedFromUrl,
|
|
951
|
+
defaultProvider: 'anthropic',
|
|
952
|
+
defaultModel: 'claude-sonnet-4-6',
|
|
953
|
+
};
|
|
954
|
+
if (req.url === '/api/quickstart/fetch-seed') {
|
|
955
|
+
await handleFetchSeed(req, res, body, quickstartDeps);
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
if (req.url === '/api/quickstart/compile-from-seed') {
|
|
959
|
+
await handleCompileFromSeed(req, res, body, quickstartDeps);
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
if (req.url === '/api/quickstart/generate-leaders') {
|
|
963
|
+
await handleGenerateLeaders(req, res, body, quickstartDeps);
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
967
|
+
res.end(JSON.stringify({ error: `Unknown quickstart route: ${req.url}` }));
|
|
968
|
+
}
|
|
969
|
+
catch (err) {
|
|
970
|
+
writeJsonError(res, err);
|
|
971
|
+
}
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
// Tier 4 T4.2: POST /simulate one-shot HTTP endpoint. Gated behind
|
|
975
|
+
// `PARACOSM_ENABLE_SIMULATE_ENDPOINT=true` so the hosted demo's
|
|
976
|
+
// SSE path stays the default; self-hosted deployments flip the
|
|
977
|
+
// flag on to expose the sync request-response surface.
|
|
978
|
+
if (req.url === '/simulate' &&
|
|
979
|
+
req.method === 'POST' &&
|
|
980
|
+
(env.PARACOSM_ENABLE_SIMULATE_ENDPOINT || '').toLowerCase() === 'true') {
|
|
981
|
+
try {
|
|
982
|
+
const body = JSON.parse(await readBody(req, maxRequestBodyBytes));
|
|
983
|
+
const userApiKey = typeof req.headers['x-api-key'] === 'string' ? req.headers['x-api-key'] : undefined;
|
|
984
|
+
const userAnthropicKey = typeof req.headers['x-anthropic-key'] === 'string' ? req.headers['x-anthropic-key'] : undefined;
|
|
985
|
+
const hasUserKeys = !!(userApiKey || userAnthropicKey);
|
|
986
|
+
// Rate limit with BYO-key bypass, matching the /setup pattern.
|
|
987
|
+
// `.check()` alone is advisory; `.record()` consumes the slot.
|
|
988
|
+
if (rateLimiter && !hasUserKeys) {
|
|
989
|
+
const ip = IpRateLimiter.getIp(req);
|
|
990
|
+
const { allowed, remaining, limit } = rateLimiter.check(ip);
|
|
991
|
+
if (!allowed) {
|
|
992
|
+
res.writeHead(429, {
|
|
993
|
+
...corsHeaders,
|
|
994
|
+
'Content-Type': 'application/json',
|
|
995
|
+
'Retry-After': '86400',
|
|
996
|
+
'X-RateLimit-Limit': String(limit),
|
|
997
|
+
'X-RateLimit-Remaining': '0',
|
|
998
|
+
});
|
|
999
|
+
res.end(JSON.stringify({
|
|
1000
|
+
error: `Rate limit exceeded. Maximum ${limit} simulations per day. Set X-API-Key or X-Anthropic-Key to bypass.`,
|
|
1001
|
+
limit,
|
|
1002
|
+
remaining: 0,
|
|
1003
|
+
}));
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
rateLimiter.record(ip);
|
|
1007
|
+
console.log(` [rate-limit] /simulate ${ip}: ${remaining - 1} remaining of ${limit}`);
|
|
1008
|
+
}
|
|
1009
|
+
else if (hasUserKeys) {
|
|
1010
|
+
console.log(` [rate-limit] /simulate bypassed. BYO key present.`);
|
|
1011
|
+
}
|
|
1012
|
+
// Scope user-supplied API keys into the env for the duration of
|
|
1013
|
+
// the simulate call. The orchestrator's downstream LLM providers
|
|
1014
|
+
// read `process.env.OPENAI_API_KEY` / `ANTHROPIC_API_KEY`; passing
|
|
1015
|
+
// the keys through `RunOptions` does nothing because RunOptions
|
|
1016
|
+
// does not declare those fields. Mirrors the inline `scopeKey`
|
|
1017
|
+
// pattern used at the /setup handler around line 1732.
|
|
1018
|
+
const simulateEnvSnapshot = [];
|
|
1019
|
+
const scopeSimulateKey = (envName, userValue) => {
|
|
1020
|
+
if (!userValue || userValue.includes('...'))
|
|
1021
|
+
return;
|
|
1022
|
+
simulateEnvSnapshot.push([envName, env[envName]]);
|
|
1023
|
+
env[envName] = userValue;
|
|
1024
|
+
};
|
|
1025
|
+
scopeSimulateKey('OPENAI_API_KEY', userApiKey);
|
|
1026
|
+
scopeSimulateKey('ANTHROPIC_API_KEY', userAnthropicKey);
|
|
1027
|
+
const restoreSimulateEnv = () => {
|
|
1028
|
+
for (const [name, prior] of simulateEnvSnapshot) {
|
|
1029
|
+
if (prior === undefined)
|
|
1030
|
+
delete env[name];
|
|
1031
|
+
else
|
|
1032
|
+
env[name] = prior;
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
const deps = {
|
|
1036
|
+
compileScenario: (raw, opts) => {
|
|
1037
|
+
const userCompile = options.compileScenario;
|
|
1038
|
+
if (userCompile)
|
|
1039
|
+
return userCompile(raw, opts);
|
|
1040
|
+
return compileScenarioReal(raw, opts);
|
|
1041
|
+
},
|
|
1042
|
+
runSimulation: async (leader, keyPersonnel, runOpts) => {
|
|
1043
|
+
const { runSimulation } = await import('../runtime/orchestrator.js');
|
|
1044
|
+
return runSimulation(leader, keyPersonnel, runOpts);
|
|
1045
|
+
},
|
|
1046
|
+
};
|
|
1047
|
+
try {
|
|
1048
|
+
await handleSimulate(req, res, body, deps);
|
|
1049
|
+
}
|
|
1050
|
+
finally {
|
|
1051
|
+
restoreSimulateEnv();
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
catch (err) {
|
|
1055
|
+
writeJsonError(res, err);
|
|
747
1056
|
}
|
|
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
1057
|
return;
|
|
753
1058
|
}
|
|
754
1059
|
// Compile a custom scenario JSON into a ScenarioPackage
|
|
755
1060
|
if (req.url === '/compile' && req.method === 'POST') {
|
|
756
1061
|
try {
|
|
757
|
-
const body = JSON.parse(await readBody(req));
|
|
1062
|
+
const body = JSON.parse(await readBody(req, maxRequestBodyBytes));
|
|
758
1063
|
const { scenario: scenarioJson, provider: requestedProvider, model: requestedModel, seedText, seedUrl, webSearch, maxSearches, apiKey, anthropicKey } = body;
|
|
759
1064
|
if (!scenarioJson || typeof scenarioJson !== 'object') {
|
|
760
1065
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
@@ -893,8 +1198,7 @@ export function createMarsServer(options = {}) {
|
|
|
893
1198
|
}
|
|
894
1199
|
catch (err) {
|
|
895
1200
|
if (!res.headersSent) {
|
|
896
|
-
res
|
|
897
|
-
res.end(JSON.stringify({ error: String(err) }));
|
|
1201
|
+
writeJsonError(res, err, 500);
|
|
898
1202
|
}
|
|
899
1203
|
else {
|
|
900
1204
|
res.write(`event: error\ndata: ${JSON.stringify({ error: String(err) })}\n\n`);
|
|
@@ -934,7 +1238,7 @@ export function createMarsServer(options = {}) {
|
|
|
934
1238
|
}
|
|
935
1239
|
};
|
|
936
1240
|
try {
|
|
937
|
-
const { agentId, message, apiKey, anthropicKey } = JSON.parse(await readBody(req));
|
|
1241
|
+
const { agentId, message, apiKey, anthropicKey } = JSON.parse(await readBody(req, maxRequestBodyBytes));
|
|
938
1242
|
if (!agentId || !message) {
|
|
939
1243
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
940
1244
|
res.end(JSON.stringify({ error: 'agentId and message required' }));
|
|
@@ -1069,8 +1373,7 @@ export function createMarsServer(options = {}) {
|
|
|
1069
1373
|
restoreChatEnv();
|
|
1070
1374
|
}
|
|
1071
1375
|
catch { }
|
|
1072
|
-
res
|
|
1073
|
-
res.end(JSON.stringify({ error: String(err) }));
|
|
1376
|
+
writeJsonError(res, err, 500);
|
|
1074
1377
|
}
|
|
1075
1378
|
return;
|
|
1076
1379
|
}
|
|
@@ -1079,105 +1382,8 @@ export function createMarsServer(options = {}) {
|
|
|
1079
1382
|
// get the same rich data the dashboard sees, without having to scrape
|
|
1080
1383
|
// raw events themselves.
|
|
1081
1384
|
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: [], systemsSnapshots: [] });
|
|
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 time = data.time;
|
|
1122
|
-
const eventIndex = data.eventIndex;
|
|
1123
|
-
const pendKey = `${leader}-${turn}-${eventIndex ?? 0}`;
|
|
1124
|
-
if (type === 'event_start') {
|
|
1125
|
-
slot.events.push({ turn, time, 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, time, 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, time, 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, time, 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, time, eventIndex, ...data });
|
|
1159
|
-
}
|
|
1160
|
-
else if (type === 'agent_reactions') {
|
|
1161
|
-
slot.agentReactions.push({ turn, time, reactions: data.reactions, totalReactions: data.totalReactions });
|
|
1162
|
-
}
|
|
1163
|
-
else if (type === 'promotion') {
|
|
1164
|
-
slot.promotions.push({ ...data });
|
|
1165
|
-
}
|
|
1166
|
-
else if (type === 'systems_snapshot') {
|
|
1167
|
-
slot.systemsSnapshots.push({ turn, time, ...data });
|
|
1168
|
-
}
|
|
1169
|
-
}
|
|
1170
|
-
const leaders = [...byLeader.entries()].map(([name, slot]) => ({ name, ...slot }));
|
|
1171
1385
|
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
|
-
}));
|
|
1386
|
+
res.end(JSON.stringify(buildResultsPayloadFromEventBuffer(eventBuffer)));
|
|
1181
1387
|
return;
|
|
1182
1388
|
}
|
|
1183
1389
|
if (req.url === '/rate-limit' && req.method === 'GET') {
|
|
@@ -1386,7 +1592,7 @@ export function createMarsServer(options = {}) {
|
|
|
1386
1592
|
}
|
|
1387
1593
|
if (req.url === '/setup' && req.method === 'POST') {
|
|
1388
1594
|
try {
|
|
1389
|
-
const config = JSON.parse(await readBody(req));
|
|
1595
|
+
const config = JSON.parse(await readBody(req, maxRequestBodyBytes));
|
|
1390
1596
|
// Rate limit check: bypass when user provides their own API keys
|
|
1391
1597
|
const hasUserKeys = !!(config.apiKey || config.anthropicKey);
|
|
1392
1598
|
if (rateLimiter && !hasUserKeys) {
|
|
@@ -1433,22 +1639,22 @@ export function createMarsServer(options = {}) {
|
|
|
1433
1639
|
// parent artifact before spinning up the orchestrator.
|
|
1434
1640
|
if (simConfig.forkFrom) {
|
|
1435
1641
|
const { parentArtifact } = simConfig.forkFrom;
|
|
1436
|
-
const
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
if (!Array.isArray(snapshots) || snapshots.length === 0) {
|
|
1446
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1642
|
+
const forkTurn = simConfig.forkFrom.atTurn;
|
|
1643
|
+
const forkValidation = validateForkSetupPreconditions({
|
|
1644
|
+
parentArtifact,
|
|
1645
|
+
atTurn: forkTurn,
|
|
1646
|
+
activeScenarioId: activeScenario.id,
|
|
1647
|
+
activeRunInProgress: simRunning && !!activeSimAbortController,
|
|
1648
|
+
});
|
|
1649
|
+
if (!forkValidation.ok) {
|
|
1650
|
+
res.writeHead(forkValidation.statusCode, { 'Content-Type': 'application/json' });
|
|
1447
1651
|
res.end(JSON.stringify({
|
|
1448
|
-
error:
|
|
1652
|
+
error: forkValidation.error,
|
|
1653
|
+
issues: forkValidation.issues,
|
|
1449
1654
|
}));
|
|
1450
1655
|
return;
|
|
1451
1656
|
}
|
|
1657
|
+
simConfig.forkFrom.parentArtifact = forkValidation.parentArtifact;
|
|
1452
1658
|
// Force captureSnapshots on for forked children so they are
|
|
1453
1659
|
// themselves forkable.
|
|
1454
1660
|
simConfig.captureSnapshots = true;
|
|
@@ -1465,6 +1671,13 @@ export function createMarsServer(options = {}) {
|
|
|
1465
1671
|
console.log(` [demo-mode] Capped run: turns=${simConfig.turns} pop=${simConfig.initialPopulation} ` +
|
|
1466
1672
|
`depts=${simConfig.activeDepartments.length} models=${simConfig.models.commander}`);
|
|
1467
1673
|
}
|
|
1674
|
+
// Build a base RunRecord for the /setup response (line ~1830). The
|
|
1675
|
+
// record is NOT inserted at run-start; instead, server-app inserts
|
|
1676
|
+
// one enriched record per completed artifact via the onArtifact
|
|
1677
|
+
// callback wired into the runner functions below. That callback
|
|
1678
|
+
// captures artifact-derived fields (artifactPath, costUSD,
|
|
1679
|
+
// durationMs, mode, leaderName, leaderArchetype) which the Library
|
|
1680
|
+
// tab needs to render gallery cards and to load full artifacts.
|
|
1468
1681
|
const runRecord = createRunRecord({
|
|
1469
1682
|
scenarioId: activeScenario.id,
|
|
1470
1683
|
scenarioVersion: activeScenario.version,
|
|
@@ -1477,12 +1690,32 @@ export function createMarsServer(options = {}) {
|
|
|
1477
1690
|
sourceMode: serverMode,
|
|
1478
1691
|
createdBy: hasUserKeys ? 'user' : 'anonymous',
|
|
1479
1692
|
});
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1693
|
+
// Insert a per-artifact RunRecord at run-end: the runner fires
|
|
1694
|
+
// `onArtifact` for each completed leader, we enrich the base
|
|
1695
|
+
// record with artifact fields and use the artifact's own runId
|
|
1696
|
+
// so the Library tab links each card to a specific artifact.
|
|
1697
|
+
// Capture the resolved simConfig values now so the closure does
|
|
1698
|
+
// not depend on the (nullable) closure-level `simConfig` later.
|
|
1699
|
+
const persistTurns = simConfig.turns;
|
|
1700
|
+
const persistSeed = simConfig.seed;
|
|
1701
|
+
const onArtifactPersist = async (artifact, leader) => {
|
|
1702
|
+
try {
|
|
1703
|
+
const perArtifactBase = {
|
|
1704
|
+
...runRecord,
|
|
1705
|
+
runId: artifact.metadata.runId,
|
|
1706
|
+
leaderConfigHash: hashLeaderConfig({
|
|
1707
|
+
leader,
|
|
1708
|
+
turns: persistTurns,
|
|
1709
|
+
seed: persistSeed,
|
|
1710
|
+
}),
|
|
1711
|
+
};
|
|
1712
|
+
const enriched = enrichRunRecordFromArtifact(perArtifactBase, artifact);
|
|
1713
|
+
await runHistoryStore.insertRun(enriched);
|
|
1714
|
+
}
|
|
1715
|
+
catch (error) {
|
|
1716
|
+
console.warn('[run-history] per-artifact insert failed:', error);
|
|
1717
|
+
}
|
|
1718
|
+
};
|
|
1486
1719
|
// Key-scope safety: snapshot the env values we're about to mutate
|
|
1487
1720
|
// so we can restore them when THIS sim ends. Without this, user A's
|
|
1488
1721
|
// key persists in process.env after their sim completes; user B's
|
|
@@ -1546,7 +1779,7 @@ export function createMarsServer(options = {}) {
|
|
|
1546
1779
|
// Start the sim and restore env when it finishes — prevents the
|
|
1547
1780
|
// caller's keys from leaking into any subsequent /setup from a
|
|
1548
1781
|
// different user that doesn't pass their own keys.
|
|
1549
|
-
marsServer.startWithConfig(simConfig).finally(restoreEnv);
|
|
1782
|
+
marsServer.startWithConfig(simConfig, { onArtifact: onArtifactPersist }).finally(restoreEnv);
|
|
1550
1783
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1551
1784
|
res.end(JSON.stringify({
|
|
1552
1785
|
redirect: '/sim',
|
|
@@ -1560,8 +1793,7 @@ export function createMarsServer(options = {}) {
|
|
|
1560
1793
|
}));
|
|
1561
1794
|
}
|
|
1562
1795
|
catch (error) {
|
|
1563
|
-
res
|
|
1564
|
-
res.end(JSON.stringify({ error: String(error) }));
|
|
1796
|
+
writeJsonError(res, error);
|
|
1565
1797
|
}
|
|
1566
1798
|
return;
|
|
1567
1799
|
}
|
|
@@ -1581,6 +1813,19 @@ export function createMarsServer(options = {}) {
|
|
|
1581
1813
|
return;
|
|
1582
1814
|
}
|
|
1583
1815
|
}
|
|
1816
|
+
// Serve demo videos (Remotion-rendered loops shown on the landing page).
|
|
1817
|
+
// Long cache: rendered output is content-addressable enough that the
|
|
1818
|
+
// landing's <video src> changes when content changes.
|
|
1819
|
+
if (req.url?.split('?')[0].startsWith('/demo/')) {
|
|
1820
|
+
const demoPath = resolve(__dirname, '..', '..', 'assets', 'demo', req.url.split('?')[0].replace('/demo/', ''));
|
|
1821
|
+
if (existsSync(demoPath)) {
|
|
1822
|
+
const ext = demoPath.split('.').pop() || '';
|
|
1823
|
+
const types = { mp4: 'video/mp4', webm: 'video/webm', png: 'image/png', jpg: 'image/jpeg' };
|
|
1824
|
+
res.writeHead(200, { 'Content-Type': types[ext] || 'application/octet-stream', 'Cache-Control': 'public, max-age=86400' });
|
|
1825
|
+
res.end(readFileSync(demoPath));
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1584
1829
|
if (req.url === '/favicon.svg' || req.url === '/favicon.png' || req.url === '/favicon.ico' || req.url === '/icon.svg' || req.url === '/apple-touch-icon.png') {
|
|
1585
1830
|
try {
|
|
1586
1831
|
const assetsDir = resolve(__dirname, '..', '..', 'assets');
|
|
@@ -1843,7 +2088,7 @@ export function createMarsServer(options = {}) {
|
|
|
1843
2088
|
server.on('close', () => { if (rateLimiter)
|
|
1844
2089
|
rateLimiter.destroy(); });
|
|
1845
2090
|
const marsServer = Object.assign(server, {
|
|
1846
|
-
async startWithConfig(config) {
|
|
2091
|
+
async startWithConfig(config, hooks) {
|
|
1847
2092
|
// Clear previous run data
|
|
1848
2093
|
clearEventBuffer();
|
|
1849
2094
|
simConfig = config;
|
|
@@ -1864,10 +2109,15 @@ export function createMarsServer(options = {}) {
|
|
|
1864
2109
|
// runForkSimulation instead of the pair runner when the
|
|
1865
2110
|
// config carries a forkFrom reference.
|
|
1866
2111
|
if (config.forkFrom) {
|
|
1867
|
-
await runForkSimulation(config, broadcast, controller.signal, activeScenario);
|
|
2112
|
+
await runForkSimulation(config, broadcast, controller.signal, activeScenario, hooks?.onArtifact);
|
|
2113
|
+
}
|
|
2114
|
+
else if (config.leaders.length >= 3) {
|
|
2115
|
+
// Tier 5 Quickstart dispatches N >= 3 leaders to the batch
|
|
2116
|
+
// runner (same SSE contract per leader, no verdict).
|
|
2117
|
+
await runBatchSimulations(config, broadcast, controller.signal, activeScenario, hooks?.onArtifact);
|
|
1868
2118
|
}
|
|
1869
2119
|
else {
|
|
1870
|
-
await startSimulations(config, broadcast, controller.signal, activeScenario);
|
|
2120
|
+
await startSimulations(config, broadcast, controller.signal, activeScenario, hooks?.onArtifact);
|
|
1871
2121
|
}
|
|
1872
2122
|
}
|
|
1873
2123
|
finally {
|