hoi4-agent-tools 3.0.7 → 3.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/hoi4_agent_tools/core/artifacts.js +21 -1
  3. package/dist/hoi4_agent_tools/core/condition-evaluator.js +399 -0
  4. package/dist/hoi4_agent_tools/core/condition-model.js +1 -0
  5. package/dist/hoi4_agent_tools/core/condition-schema.js +42 -0
  6. package/dist/hoi4_agent_tools/core/configuration.js +14 -28
  7. package/dist/hoi4_agent_tools/core/engine.js +7 -1
  8. package/dist/hoi4_agent_tools/core/index.js +25 -0
  9. package/dist/hoi4_agent_tools/core/request-scheduler.js +79 -0
  10. package/dist/hoi4_agent_tools/core/scenario-state.js +97 -0
  11. package/dist/hoi4_agent_tools/core/shared-request-capacity.js +125 -0
  12. package/dist/hoi4_agent_tools/gui/scenario-conditions.js +119 -0
  13. package/dist/hoi4_agent_tools/gui/scenario-generator.js +59 -73
  14. package/dist/hoi4_agent_tools/gui/scenario.js +3 -0
  15. package/dist/hoi4_agent_tools/gui/studio.js +9 -3
  16. package/dist/hoi4_agent_tools/mcp/server/create.js +3 -1
  17. package/dist/hoi4_agent_tools/mcp/server/progress.js +29 -8
  18. package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +14 -0
  19. package/dist/hoi4_agent_tools/mcp/transports/bounded-stdio.js +44 -14
  20. package/dist/hoi4_agent_tools/mcp/transports/http.js +75 -7
  21. package/dist/hoi4_agent_tools/probability/scenario-state.js +2 -95
  22. package/dist/hoi4_agent_tools/probability/trigger-evaluator.js +1 -287
  23. package/dist/hoi4_agent_tools/version.js +1 -1
  24. package/docs/development.md +2 -0
  25. package/docs/gui.md +3 -1
  26. package/docs/http.md +5 -1
  27. package/package.json +3 -2
  28. package/schemas/configuration.schema.json +151 -133
  29. package/schemas/continuous-focus-palette.schema.json +1 -1
  30. package/schemas/custom-weighted-pool.schema.json +1 -1
  31. package/schemas/event-feature-manifest.schema.json +1 -1
  32. package/schemas/focus-plan.schema.json +1 -1
  33. package/schemas/focus-planning-sidecar.schema.json +1 -1
  34. package/schemas/gui-animation-source.schema.json +1 -1
  35. package/schemas/gui-helper.schema.json +1 -1
  36. package/schemas/gui-scenario.schema.json +189 -33
  37. package/schemas/map-operation.schema.json +1 -1
  38. package/schemas/operation-result.schema.json +1 -1
  39. package/schemas/probability-analysis-result.schema.json +1 -1
  40. package/schemas/probability-compare-input.schema.json +1 -1
  41. package/schemas/probability-evaluate-input.schema.json +1 -1
  42. package/schemas/probability-inspect-input.schema.json +1 -1
  43. package/schemas/probability-render-input.schema.json +1 -1
  44. package/schemas/probability-scenario-set.schema.json +1 -1
  45. package/schemas/probability-sequence-input.schema.json +1 -1
  46. package/schemas/probability-simulate-input.schema.json +1 -1
  47. package/schemas/probability-sweep-input.schema.json +1 -1
  48. package/server.json +2 -2
package/CHANGELOG.md CHANGED
@@ -4,6 +4,32 @@
4
4
 
5
5
  No unreleased changes.
6
6
 
7
+ ## 3.0.9 - 2026-09-08
8
+
9
+ - Evaluate GUI scripted-localisation branches through the shared structured condition evaluator instead of regex threshold guesses.
10
+ - Correct inclusive boundaries, compound conditions, negation, variable operands, constants, and named scopes; retain the existing probability API through core compatibility exports.
11
+ - Resolve source-backed scripted-trigger calls, including declared scalar parameters, and retain helper provenance and unresolved conditions.
12
+ - Keep an unknown earlier localisation branch unresolved instead of selecting a ranked candidate or an unproven fallback.
13
+ - Preserve explicit values and seeded exploratory previews, expose optional named GUI scope bindings, and include per-choice condition evidence in scenario artifacts.
14
+ - Add independent condition truth-table tests and generated-scenario-to-render regressions for the reported failures, colours, scoped helpers, placeholders, and repeatable SVG/PNG output.
15
+ - Align the sparse-mod stdio test's overall budget with its RPC waits and honor matching progress notifications without removing the finite test deadline.
16
+ - Avoid Windows path-resolution races while concurrent stdio processes release capacity slots, retaining private-root validation and explicit linked-slot rejection.
17
+ - Retry Windows delete-pending capacity-slot creation without admitting duplicate work; persistent permission failures remain bounded errors.
18
+
19
+ ## 3.0.8 - 2026-09-05
20
+
21
+ - Queue simultaneous tool calls fairly across HTTP sessions and coordinate expensive operations across local task processes sharing private server state.
22
+ - Rotate across every waiting session and canonicalize configured storage aliases, including Windows short-name paths, before acquiring shared execution capacity.
23
+ - Keep every tool call responsive with strictly increasing MCP progress notifications during execution and queue waits; allow cancellation and discovery while tool capacity is occupied.
24
+ - Separate HTTP request admission and body-byte reservations from expensive execution capacity, replacing the two-request ceiling with defaults suitable for parallel agents.
25
+ - Keep active HTTP sessions alive, yield during shared source indexing, and serialize backpressured stdio output without leaking listeners or leaving sends pending after a disconnect.
26
+ - Share cached GUI source graphs across sessions using the same engine instead of retaining a separate graph per client.
27
+ - Reverify artifacts after metadata changes caused by concurrent publication-link cleanup, avoiding false hash collisions while still rejecting altered bytes.
28
+ - Serialize artifact admission, publication, and failed-batch cleanup across processes so one writer cannot remove evidence committed by another.
29
+ - Add concurrent production workflows spanning focus, GUI, map, event, technology, and probability tools over both transports, including shared artifacts, session expiry, cancelled queue entries, and abandoned process recovery.
30
+ - Qualify each map layer separately from semantic and pixel diffs so failures and deadlines identify the affected operation rather than an aggregate rendering loop.
31
+ - Update `fast-uri` and `qs` to patched releases identified by the dependency audit.
32
+
7
33
  ## 3.0.7 - 2026-09-01
8
34
 
9
35
  - Reclaim detached parser, graph, and render memory after the idle cache window so parallel coding-agent tasks do not retain large V8 heaps and trigger `Transport closed` through host memory exhaustion. Idle cleanup is coordinated across every MCP domain and never compacts during an active tool call.
@@ -4,6 +4,7 @@ import path from 'node:path';
4
4
  import { z } from 'zod/v4';
5
5
  import { canonicalJson, compareCodeUnits, sha256Bytes } from './canonical.js';
6
6
  import { ServiceError } from './result.js';
7
+ import { SharedRequestCapacity } from './shared-request-capacity.js';
7
8
  import { containedGeneratedPath, isPortablePathSegment } from './workspace.js';
8
9
  const sha256Schema = z.string().regex(/^[a-f0-9]{64}$/u);
9
10
  const artifactProvenanceSchema = z
@@ -329,7 +330,9 @@ async function withArtifactQueue(root, action, signal) {
329
330
  artifactQueues.set(root, tail);
330
331
  try {
331
332
  await waitForArtifactTurn(previous, signal);
332
- return await action();
333
+ // Admission, publication, and rollback must be one critical section across
334
+ // processes too: another writer may reuse the same immutable address.
335
+ return await new SharedRequestCapacity(root, 1).run(signal ?? new AbortController().signal, action);
333
336
  }
334
337
  finally {
335
338
  release();
@@ -607,6 +610,23 @@ async function publishExclusiveFile(target, bytes, signal) {
607
610
  }
608
611
  }
609
612
  async function readVerifiedArtifactRange(filePath, expectedSha256, range, verifiedIdentity, signal) {
613
+ for (let attempt = 0;; attempt += 1) {
614
+ try {
615
+ return await readArtifactRangeOnce(filePath, expectedSha256, range, attempt === 0 ? verifiedIdentity : undefined, signal);
616
+ }
617
+ catch (error) {
618
+ if (attempt >= 2 ||
619
+ !(error instanceof ServiceError) ||
620
+ error.code !== 'ARTIFACT_READ_INCOMPLETE')
621
+ throw error;
622
+ // Removing the temporary hard link after atomic publication changes ctime
623
+ // without changing content. Retry from a fresh handle and hash all bytes;
624
+ // never reuse cached verification after an identity change.
625
+ await new Promise((resolve) => setImmediate(resolve));
626
+ }
627
+ }
628
+ }
629
+ async function readArtifactRangeOnce(filePath, expectedSha256, range, verifiedIdentity, signal) {
610
630
  signal?.throwIfAborted();
611
631
  if (range !== undefined &&
612
632
  (!Number.isSafeInteger(range.offset) ||
@@ -0,0 +1,399 @@
1
+ import { assignments, astPathFor, firstScalar, nodeLocation, } from './source/index.js';
2
+ import { resolveScopeContext, rootScopeContext, rootStateValue, scopeIdentity, scopeStateValue, } from './scenario-state.js';
3
+ function helperWithArguments(block, invocation) {
4
+ const parameters = new Map();
5
+ for (const entry of invocation.entries) {
6
+ if (entry.type !== 'assignment' || entry.value.type !== 'scalar')
7
+ return undefined;
8
+ parameters.set(entry.key.value, entry.value.value);
9
+ }
10
+ const missing = new Set();
11
+ const substitute = (value) => value.replace(/\$([A-Za-z0-9_]+)\$/gu, (match, key) => {
12
+ const replacement = parameters.get(key);
13
+ if (replacement === undefined) {
14
+ missing.add(key);
15
+ return match;
16
+ }
17
+ return replacement;
18
+ });
19
+ const visit = (entry) => {
20
+ if (entry.type === 'scalar')
21
+ return { ...entry, value: substitute(entry.value) };
22
+ if (entry.type === 'block')
23
+ return { ...entry, entries: entry.entries.map(visit) };
24
+ return {
25
+ ...entry,
26
+ key: { ...entry.key, value: substitute(entry.key.value) },
27
+ value: entry.value.type === 'scalar'
28
+ ? { ...entry.value, value: substitute(entry.value.value) }
29
+ : { ...entry.value, entries: entry.value.entries.map(visit) },
30
+ };
31
+ };
32
+ const expanded = { ...block, entries: block.entries.map(visit) };
33
+ return missing.size === 0 ? expanded : undefined;
34
+ }
35
+ function combineAnd(values) {
36
+ if (values.includes('false'))
37
+ return 'false';
38
+ return values.includes('unresolved') ? 'unresolved' : 'true';
39
+ }
40
+ function combineOr(values) {
41
+ if (values.includes('true'))
42
+ return 'true';
43
+ return values.includes('unresolved') ? 'unresolved' : 'false';
44
+ }
45
+ function invert(value) {
46
+ return value === 'true' ? 'false' : value === 'false' ? 'true' : 'unresolved';
47
+ }
48
+ function bool(value) {
49
+ if (typeof value === 'boolean')
50
+ return value;
51
+ if (value === 'yes' || value === 'true')
52
+ return true;
53
+ if (value === 'no' || value === 'false')
54
+ return false;
55
+ return undefined;
56
+ }
57
+ function numeric(value) {
58
+ if (typeof value === 'number' && Number.isFinite(value))
59
+ return value;
60
+ if (typeof value === 'string' && value.trim() !== '' && Number.isFinite(Number(value)))
61
+ return Number(value);
62
+ return undefined;
63
+ }
64
+ function compare(left, right, operator) {
65
+ if (typeof left === 'number' && typeof right === 'number') {
66
+ if (operator === '>')
67
+ return left > right;
68
+ if (operator === '<')
69
+ return left < right;
70
+ }
71
+ return operator === '!=' ? left !== right : left === right;
72
+ }
73
+ function numberValue(expression, scenario, context, definitions, path, seen = new Set()) {
74
+ const literal = numeric(expression);
75
+ if (literal !== undefined)
76
+ return literal;
77
+ const identity = `${path ?? ''}:${expression}`;
78
+ if (seen.has(identity))
79
+ return undefined;
80
+ seen.add(identity);
81
+ const constant = expression.startsWith('constant:')
82
+ ? definitions.scriptConstants.get(expression.slice(9))
83
+ : expression.startsWith('@') && path !== undefined
84
+ ? definitions.localConstants.get(path)?.get(expression)
85
+ : undefined;
86
+ if (constant !== undefined)
87
+ return numberValue(constant.value, scenario, context, definitions, constant.file.displayPath, seen);
88
+ const direct = scopeStateValue(scenario, expression, context) ??
89
+ scopeStateValue(scenario, `variable.${expression}`, context);
90
+ if (direct !== undefined)
91
+ return numeric(direct);
92
+ for (let split = expression.lastIndexOf('.'); split > 0; split = expression.lastIndexOf('.', split - 1)) {
93
+ const scope = resolveScopeContext(scenario, expression.slice(0, split), context);
94
+ if (scope !== undefined)
95
+ return numberValue(expression.slice(split + 1), scenario, scope, definitions, path, seen);
96
+ }
97
+ return undefined;
98
+ }
99
+ function unresolved(assignment, candidate, reason) {
100
+ return {
101
+ state: 'unresolved',
102
+ unresolved: [
103
+ {
104
+ code: 'TRIGGER_UNRESOLVED',
105
+ message: reason,
106
+ path: assignment.key.value,
107
+ candidateId: candidate.id,
108
+ ...(candidate.provenance[0] === undefined ? {} : { provenance: candidate.provenance[0] }),
109
+ },
110
+ ],
111
+ };
112
+ }
113
+ function malformedBlock(block, candidate) {
114
+ if (block.entries.every(({ type }) => type === 'assignment'))
115
+ return undefined;
116
+ return {
117
+ state: 'unresolved',
118
+ unresolved: [
119
+ {
120
+ code: 'TRIGGER_UNRESOLVED',
121
+ message: 'Condition contains a bare value or block instead of a trigger assignment',
122
+ candidateId: candidate.id,
123
+ ...(candidate.provenance[0] === undefined ? {} : { provenance: candidate.provenance[0] }),
124
+ },
125
+ ],
126
+ };
127
+ }
128
+ function evaluateCheckVariable(block, scenario, candidate, assignment, scopeContext, definitions) {
129
+ const short = assignments(block).find(({ key }) => !['var', 'value', 'compare', 'tooltip'].includes(key.value));
130
+ const variable = firstScalar(block, 'var')?.value ?? short?.key.value;
131
+ const target = firstScalar(block, 'value')?.value ??
132
+ (short?.value.type === 'scalar' ? short.value.value : undefined);
133
+ if (variable === undefined || target === undefined)
134
+ return unresolved(assignment, candidate, 'check_variable requires declared var and value');
135
+ const path = candidate.provenance[0]?.path;
136
+ const leftNumber = numberValue(variable, scenario, scopeContext, definitions, path);
137
+ const rightNumber = numberValue(target, scenario, scopeContext, definitions, path);
138
+ if (leftNumber === undefined || rightNumber === undefined)
139
+ return unresolved(assignment, candidate, `Scenario does not resolve numeric operands ${variable} and ${target}`);
140
+ const compareName = firstScalar(block, 'compare')?.value ??
141
+ (short?.operator.text === '>'
142
+ ? 'greater_than'
143
+ : short?.operator.text === '<'
144
+ ? 'less_than'
145
+ : 'equals');
146
+ if (![
147
+ 'greater_than',
148
+ 'greater_than_or_equals',
149
+ 'less_than',
150
+ 'less_than_or_equals',
151
+ 'not_equals',
152
+ 'equals',
153
+ ].includes(compareName))
154
+ return unresolved(assignment, candidate, `Unsupported check_variable comparison ${compareName}`);
155
+ const result = compareName === 'greater_than'
156
+ ? leftNumber > rightNumber
157
+ : compareName === 'greater_than_or_equals'
158
+ ? leftNumber >= rightNumber
159
+ : compareName === 'less_than'
160
+ ? leftNumber < rightNumber
161
+ : compareName === 'less_than_or_equals'
162
+ ? leftNumber <= rightNumber
163
+ : compareName === 'not_equals'
164
+ ? leftNumber !== rightNumber
165
+ : leftNumber === rightNumber;
166
+ return { state: result ? 'true' : 'false', unresolved: [] };
167
+ }
168
+ function evaluateAssignmentCore(assignment, scenario, candidate, definitions, helperStack, scopeContext) {
169
+ const key = assignment.key.value;
170
+ if (assignment.value.type === 'block') {
171
+ const malformed = malformedBlock(assignment.value, candidate);
172
+ if (malformed !== undefined)
173
+ return malformed;
174
+ if (key === 'AND')
175
+ return evaluateTriggerBlock(assignment.value, scenario, candidate, definitions, helperStack, scopeContext);
176
+ if (key === 'OR') {
177
+ const children = assignments(assignment.value).map((child) => evaluateAssignment(child, scenario, candidate, definitions, helperStack, scopeContext));
178
+ return {
179
+ state: combineOr(children.map(({ state }) => state)),
180
+ unresolved: children.flatMap(({ unresolved: items }) => items),
181
+ helperProvenance: children.flatMap(({ helperProvenance }) => helperProvenance ?? []),
182
+ };
183
+ }
184
+ if (key === 'NOT' || key === 'NOR' || key === 'NAND') {
185
+ const children = assignments(assignment.value).map((child) => evaluateAssignment(child, scenario, candidate, definitions, helperStack, scopeContext));
186
+ const base = key === 'NAND'
187
+ ? combineAnd(children.map(({ state }) => state))
188
+ : combineOr(children.map(({ state }) => state));
189
+ return {
190
+ state: invert(base),
191
+ unresolved: children.flatMap(({ unresolved: items }) => items),
192
+ helperProvenance: children.flatMap(({ helperProvenance }) => helperProvenance ?? []),
193
+ };
194
+ }
195
+ if (key === 'check_variable')
196
+ return evaluateCheckVariable(assignment.value, scenario, candidate, assignment, scopeContext, definitions);
197
+ if (key === 'is_in_array' || key === 'is_variable_in_array') {
198
+ const arrayId = firstScalar(assignment.value, 'array')?.value;
199
+ const declaredValue = firstScalar(assignment.value, 'value')?.value;
200
+ if (arrayId === undefined || declaredValue === undefined)
201
+ return unresolved(assignment, candidate, `${key} requires array and value`);
202
+ const array = scopeStateValue(scenario, `array.${arrayId}`, scopeContext) ??
203
+ rootStateValue(scenario, `array.${arrayId}`);
204
+ if (!Array.isArray(array))
205
+ return unresolved(assignment, candidate, `Scenario does not declare array ${arrayId}`);
206
+ const compared = ['THIS', 'ROOT', 'PREV'].includes(declaredValue.toUpperCase()) ||
207
+ resolveScopeContext(scenario, declaredValue, scopeContext) !== undefined
208
+ ? scopeIdentity(resolveScopeContext(scenario, declaredValue, scopeContext) ?? scopeContext)
209
+ : (scopeStateValue(scenario, declaredValue, scopeContext) ?? declaredValue);
210
+ return { state: array.includes(compared) ? 'true' : 'false', unresolved: [] };
211
+ }
212
+ const resolvedScope = resolveScopeContext(scenario, key, scopeContext);
213
+ if (resolvedScope !== undefined)
214
+ return evaluateTriggerBlock(assignment.value, scenario, candidate, definitions, helperStack, resolvedScope);
215
+ const helper = definitions.scriptedTriggers.get(key);
216
+ if (helper !== undefined) {
217
+ if (helperStack.includes(key))
218
+ return unresolved(assignment, candidate, `Scripted trigger recursion detected at ${key}`);
219
+ const expanded = helperWithArguments(helper.value, assignment.value);
220
+ if (expanded === undefined)
221
+ return unresolved(assignment, candidate, `Scripted trigger ${key} requires declared scalar arguments for every parameter`);
222
+ const helperProvenance = {
223
+ path: helper.file.displayPath,
224
+ rootKind: helper.file.rootKind,
225
+ loadOrder: helper.file.loadOrder,
226
+ sourceHash: helper.file.sha256,
227
+ location: nodeLocation(helper.document, helper.node, key),
228
+ ...(astPathFor(helper.document, helper.node) === undefined
229
+ ? {}
230
+ : { astPath: astPathFor(helper.document, helper.node) }),
231
+ symbol: key,
232
+ helperChain: [...helperStack, key],
233
+ };
234
+ const evaluated = evaluateTriggerBlock(expanded, scenario, {
235
+ ...candidate,
236
+ document: helper.document,
237
+ provenance: [helperProvenance, ...candidate.provenance],
238
+ }, definitions, [...helperStack, key], scopeContext);
239
+ return {
240
+ ...evaluated,
241
+ helperProvenance: [helperProvenance, ...(evaluated.helperProvenance ?? [])],
242
+ };
243
+ }
244
+ const declaredScope = bool(rootStateValue(scenario, `scope.${key}`));
245
+ if (declaredScope !== undefined)
246
+ return { state: declaredScope ? 'true' : 'false', unresolved: [] };
247
+ return unresolved(assignment, candidate, `Scoped or compound trigger ${key} is not declared by the scenario`);
248
+ }
249
+ const right = assignment.value.value;
250
+ if (key === 'always') {
251
+ const value = bool(right);
252
+ return value === undefined
253
+ ? unresolved(assignment, candidate, 'always trigger is not boolean')
254
+ : { state: value ? 'true' : 'false', unresolved: [] };
255
+ }
256
+ if (key === 'has_country_flag' || key === 'has_global_flag' || key === 'has_state_flag') {
257
+ const explicit = bool(scopeStateValue(scenario, `flag.${right}`, scopeContext));
258
+ if (explicit !== undefined)
259
+ return { state: explicit ? 'true' : 'false', unresolved: [] };
260
+ const flags = scopeContext.expression === 'ROOT' ? scenario.flags : scopeContext.binding.flags;
261
+ if (scenario.closedFlags === false && flags?.includes(right) !== true)
262
+ return unresolved(assignment, candidate, `Scenario does not declare flag ${right}`);
263
+ return { state: flags?.includes(right) === true ? 'true' : 'false', unresolved: [] };
264
+ }
265
+ if (key === 'has_event_target') {
266
+ const eventTargets = scopeContext.binding.eventTargets ?? scenario.eventTargets;
267
+ return {
268
+ state: Object.hasOwn(eventTargets ?? {}, right) ? 'true' : 'false',
269
+ unresolved: [],
270
+ };
271
+ }
272
+ if (key === 'tag' || key === 'original_tag') {
273
+ const actor = scopeContext.expression === 'ROOT'
274
+ ? scenario.actor
275
+ : (scopeContext.binding.actor ?? scopeContext.binding.id);
276
+ if (actor === undefined)
277
+ return unresolved(assignment, candidate, `Scenario does not declare actor for ${key}`);
278
+ return {
279
+ state: compare(actor, right, assignment.operator.text) ? 'true' : 'false',
280
+ unresolved: [],
281
+ };
282
+ }
283
+ if (key === 'date') {
284
+ if (scenario.date === undefined)
285
+ return unresolved(assignment, candidate, 'Scenario does not declare date');
286
+ const dateNumber = (value) => {
287
+ const parts = /^(\d+)\.(\d{1,2})\.(\d{1,2})$/u.exec(value);
288
+ if (parts === null)
289
+ return undefined;
290
+ const month = Number(parts[2]);
291
+ const day = Number(parts[3]);
292
+ return month < 1 || month > 12 || day < 1 || day > 31
293
+ ? undefined
294
+ : Number(parts[1]) * 10_000 + month * 100 + day;
295
+ };
296
+ const leftDate = dateNumber(scenario.date);
297
+ const rightDate = dateNumber(right);
298
+ if (leftDate === undefined || rightDate === undefined)
299
+ return unresolved(assignment, candidate, 'Date comparison is not numeric');
300
+ return {
301
+ state: compare(leftDate, rightDate, assignment.operator.text) ? 'true' : 'false',
302
+ unresolved: [],
303
+ };
304
+ }
305
+ const helper = definitions.scriptedTriggers.get(key);
306
+ if (helper !== undefined && bool(right) !== undefined) {
307
+ if (helperStack.includes(key))
308
+ return unresolved(assignment, candidate, `Scripted trigger recursion detected at ${key}`);
309
+ const helperProvenance = {
310
+ path: helper.file.displayPath,
311
+ rootKind: helper.file.rootKind,
312
+ loadOrder: helper.file.loadOrder,
313
+ sourceHash: helper.file.sha256,
314
+ location: nodeLocation(helper.document, helper.node, key),
315
+ ...(astPathFor(helper.document, helper.node) === undefined
316
+ ? {}
317
+ : { astPath: astPathFor(helper.document, helper.node) }),
318
+ symbol: key,
319
+ helperChain: [...helperStack, key],
320
+ };
321
+ const evaluated = evaluateTriggerBlock(helper.value, scenario, {
322
+ ...candidate,
323
+ document: helper.document,
324
+ provenance: [helperProvenance, ...candidate.provenance],
325
+ }, definitions, [...helperStack, key], scopeContext);
326
+ return {
327
+ ...evaluated,
328
+ state: bool(right) === false ? invert(evaluated.state) : evaluated.state,
329
+ helperProvenance: [helperProvenance, ...(evaluated.helperProvenance ?? [])],
330
+ };
331
+ }
332
+ const declared = scopeStateValue(scenario, key, scopeContext) ??
333
+ numberValue(key, scenario, scopeContext, definitions, candidate.provenance[0]?.path);
334
+ if (declared === undefined)
335
+ return unresolved(assignment, candidate, `Scenario does not declare trigger ${key}`);
336
+ const expectedBoolean = bool(right);
337
+ const actualBoolean = bool(declared);
338
+ if (expectedBoolean !== undefined && actualBoolean !== undefined)
339
+ return { state: expectedBoolean === actualBoolean ? 'true' : 'false', unresolved: [] };
340
+ const leftNumber = numeric(declared);
341
+ const rightNumber = numberValue(right, scenario, scopeContext, definitions, candidate.provenance[0]?.path);
342
+ if (leftNumber !== undefined && rightNumber !== undefined)
343
+ return {
344
+ state: compare(leftNumber, rightNumber, assignment.operator.text) ? 'true' : 'false',
345
+ unresolved: [],
346
+ };
347
+ if (typeof declared === 'string')
348
+ return {
349
+ state: compare(declared, right, assignment.operator.text) ? 'true' : 'false',
350
+ unresolved: [],
351
+ };
352
+ return unresolved(assignment, candidate, `Trigger ${key} cannot compare the declared scenario value`);
353
+ }
354
+ function evaluateAssignment(assignment, scenario, candidate, definitions, helperStack, scopeContext) {
355
+ const evaluated = evaluateAssignmentCore(assignment, scenario, candidate, definitions, helperStack, scopeContext);
356
+ const source = candidate.provenance[0];
357
+ const provenance = candidate.document === undefined || source === undefined
358
+ ? source
359
+ : {
360
+ ...source,
361
+ location: nodeLocation(candidate.document, assignment, assignment.key.value),
362
+ ...(astPathFor(candidate.document, assignment) === undefined
363
+ ? {}
364
+ : { astPath: astPathFor(candidate.document, assignment) }),
365
+ symbol: assignment.key.value,
366
+ };
367
+ const expression = assignment.value.type === 'scalar'
368
+ ? `${assignment.key.value} ${assignment.operator.text} ${assignment.value.value}`
369
+ : assignment.key.value;
370
+ return {
371
+ ...evaluated,
372
+ trace: [
373
+ {
374
+ operation: 'eligibility',
375
+ expression,
376
+ applied: evaluated.state,
377
+ ...(provenance === undefined ? {} : { provenance }),
378
+ },
379
+ ...(evaluated.trace ?? []),
380
+ ],
381
+ };
382
+ }
383
+ export function evaluateTriggerBlock(block, scenario, candidate, definitions, helperStack = [], scopeContext = rootScopeContext(scenario)) {
384
+ const override = scenario.candidateOverrides?.[candidate.id];
385
+ if (override !== undefined)
386
+ return { state: override ? 'true' : 'false', unresolved: [] };
387
+ if (block === undefined)
388
+ return { state: 'true', unresolved: [] };
389
+ const malformed = malformedBlock(block, candidate);
390
+ if (malformed !== undefined)
391
+ return malformed;
392
+ const children = assignments(block).map((assignment) => evaluateAssignment(assignment, scenario, candidate, definitions, helperStack, scopeContext));
393
+ return {
394
+ state: combineAnd(children.map(({ state }) => state)),
395
+ unresolved: children.flatMap(({ unresolved: items }) => items),
396
+ helperProvenance: children.flatMap(({ helperProvenance }) => helperProvenance ?? []),
397
+ trace: children.flatMap(({ trace }) => trace ?? []),
398
+ };
399
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,42 @@
1
+ import { z } from 'zod';
2
+ export const ScenarioScalarSchema = z.union([
3
+ z.string().max(16_384),
4
+ z.number(),
5
+ z.boolean(),
6
+ z.null(),
7
+ ]);
8
+ export const ScenarioValueSchema = z.union([
9
+ ScenarioScalarSchema,
10
+ z.array(ScenarioScalarSchema).max(10_000),
11
+ ]);
12
+ export const ConditionScopeBindingSchema = z
13
+ .object({
14
+ id: z.string().min(1).max(256),
15
+ type: z
16
+ .enum([
17
+ 'country',
18
+ 'state',
19
+ 'character',
20
+ 'unit_leader',
21
+ 'operative',
22
+ 'strategic_region',
23
+ 'province',
24
+ 'unknown',
25
+ ])
26
+ .optional(),
27
+ actor: z.string().max(256).optional(),
28
+ state: z.record(z.string(), ScenarioValueSchema).default({}),
29
+ flags: z.array(z.string().max(256)).max(10_000).optional(),
30
+ eventTargets: z.record(z.string(), z.string().max(256)).optional(),
31
+ weight: z.union([z.number(), z.string().max(256)]).optional(),
32
+ })
33
+ .strict();
34
+ export const ConditionResultSchema = z
35
+ .object({
36
+ token: z.string(),
37
+ sourcePath: z.string(),
38
+ state: z.enum(['true', 'false', 'unresolved']),
39
+ localisationKey: z.string().optional(),
40
+ unresolved: z.array(z.string()),
41
+ })
42
+ .strict();
@@ -3,7 +3,8 @@ import path from 'node:path';
3
3
  import { z } from 'zod/v4';
4
4
  import { CONFIG_VERSION } from '../version.js';
5
5
  import { ServiceError } from './result.js';
6
- export const HTTP_MAX_SAFE_CONCURRENT_REQUESTS = 2;
6
+ export const HTTP_MAX_SAFE_CONCURRENT_REQUESTS = 1024;
7
+ export const HTTP_DEFAULT_CONCURRENT_REQUESTS = 128;
7
8
  export const HTTP_MAX_BODY_BYTES = 67_108_864;
8
9
  export const HTTP_MAX_AGGREGATE_BODY_BYTES = 134_217_728;
9
10
  export const HTTP_DEFAULT_SESSION_EVENT_BYTES = 2_097_152;
@@ -132,6 +133,8 @@ export const serverConfigurationSchema = z
132
133
  })
133
134
  .optional(),
134
135
  scanMaxFiles: z.number().int().min(1).max(1_000_000).default(DEFAULT_SCAN_MAX_FILES),
136
+ maxConcurrentTools: z.number().int().min(1).max(32).default(2),
137
+ maxSharedTools: z.number().int().min(1).max(128).default(4),
135
138
  scanMaxBytes: z
136
139
  .number()
137
140
  .int()
@@ -193,18 +196,18 @@ export const serverConfigurationSchema = z
193
196
  headersTimeoutMs: z.number().int().min(1_000).max(120_000).default(10_000),
194
197
  requestTimeoutMs: z.number().int().min(1_000).max(300_000).default(30_000),
195
198
  keepAliveTimeoutMs: z.number().int().min(1_000).max(120_000).default(5_000),
196
- maxConnections: z.number().int().min(1).max(100_000).default(64),
199
+ maxConnections: z.number().int().min(1).max(100_000).default(512),
197
200
  maxRequestsPerSocket: z.number().int().min(1).max(100_000).default(100),
198
201
  maxConcurrentRequests: z
199
202
  .number()
200
203
  .int()
201
204
  .min(1)
202
205
  .max(HTTP_MAX_SAFE_CONCURRENT_REQUESTS)
203
- .default(HTTP_MAX_SAFE_CONCURRENT_REQUESTS),
206
+ .default(HTTP_DEFAULT_CONCURRENT_REQUESTS),
204
207
  maxSessions: z.number().int().min(1).max(10_000).default(128),
205
208
  maxSessionsPerPrincipal: z.number().int().min(1).max(10_000).default(32),
206
- maxEventStreams: z.number().int().min(1).max(10_000).default(32),
207
- maxEventStreamsPerPrincipal: z.number().int().min(1).max(10_000).default(4),
209
+ maxEventStreams: z.number().int().min(1).max(10_000).default(128),
210
+ maxEventStreamsPerPrincipal: z.number().int().min(1).max(10_000).default(32),
208
211
  maxSessionEventBytes: z
209
212
  .number()
210
213
  .int()
@@ -212,7 +215,7 @@ export const serverConfigurationSchema = z
212
215
  .max(67_108_864)
213
216
  .default(HTTP_DEFAULT_SESSION_EVENT_BYTES),
214
217
  maxEventStoreBytes: z.number().int().min(65_536).max(268_435_456).default(16_777_216),
215
- requestsPerMinute: z.number().int().min(1).max(100_000).default(120),
218
+ requestsPerMinute: z.number().int().min(1).max(100_000).default(6_000),
216
219
  sessionTtlSeconds: z.number().int().min(60).max(86_400).default(3600),
217
220
  })
218
221
  .strict()
@@ -274,16 +277,16 @@ export const serverConfigurationSchema = z
274
277
  headersTimeoutMs: 10_000,
275
278
  requestTimeoutMs: 30_000,
276
279
  keepAliveTimeoutMs: 5_000,
277
- maxConnections: 64,
280
+ maxConnections: 512,
278
281
  maxRequestsPerSocket: 100,
279
- maxConcurrentRequests: HTTP_MAX_SAFE_CONCURRENT_REQUESTS,
282
+ maxConcurrentRequests: HTTP_DEFAULT_CONCURRENT_REQUESTS,
280
283
  maxSessions: 128,
281
284
  maxSessionsPerPrincipal: 32,
282
- maxEventStreams: 32,
283
- maxEventStreamsPerPrincipal: 4,
285
+ maxEventStreams: 128,
286
+ maxEventStreamsPerPrincipal: 32,
284
287
  maxSessionEventBytes: HTTP_DEFAULT_SESSION_EVENT_BYTES,
285
288
  maxEventStoreBytes: 16_777_216,
286
- requestsPerMinute: 120,
289
+ requestsPerMinute: 6_000,
287
290
  sessionTtlSeconds: 3600,
288
291
  }),
289
292
  })
@@ -332,23 +335,6 @@ export const serverConfigurationSchema = z
332
335
  message: 'HTTP keep-alive timeout must be shorter than the header timeout',
333
336
  });
334
337
  }
335
- const concurrentScanBytes = value.scanMaxBytes * value.http.maxConcurrentRequests;
336
- if (!Number.isSafeInteger(concurrentScanBytes) || concurrentScanBytes > 536_870_912) {
337
- context.addIssue({
338
- code: 'custom',
339
- path: ['scanMaxBytes'],
340
- message: 'Scan-byte ceiling multiplied by HTTP concurrency must not exceed 512 MiB',
341
- });
342
- }
343
- const concurrentBodyBytes = value.http.maxBodyBytes * value.http.maxConcurrentRequests;
344
- if (!Number.isSafeInteger(concurrentBodyBytes) ||
345
- concurrentBodyBytes > HTTP_MAX_AGGREGATE_BODY_BYTES) {
346
- context.addIssue({
347
- code: 'custom',
348
- path: ['http', 'maxBodyBytes'],
349
- message: 'HTTP body-byte ceiling multiplied by concurrency must not exceed 128 MiB',
350
- });
351
- }
352
338
  });
353
339
  export async function loadConfiguration(filePath) {
354
340
  const absolute = path.resolve(filePath);
@@ -3,6 +3,8 @@ import { SymbolIndex } from './index.js';
3
3
  import { WorkspaceScanner } from './scanner.js';
4
4
  import { ArtifactStore } from './artifacts.js';
5
5
  import { TransactionManager } from './transactions.js';
6
+ import { RequestScheduler } from './request-scheduler.js';
7
+ import { SharedRequestCapacity } from './shared-request-capacity.js';
6
8
  const RECOVERY_TTL_SECONDS = 3_600;
7
9
  const RECOVERY_MAX_BYTES = 536_870_912;
8
10
  const RECOVERY_MAX_RECORDS = 128;
@@ -74,6 +76,8 @@ function defaultPatterns(workspace) {
74
76
  }
75
77
  export class CoreEngine {
76
78
  resolver;
79
+ requests;
80
+ sharedRequests;
77
81
  scanner;
78
82
  artifacts;
79
83
  transactions;
@@ -84,6 +88,8 @@ export class CoreEngine {
84
88
  #activeScanAdmissions = new Set();
85
89
  constructor(resolver, services = {}) {
86
90
  this.resolver = resolver;
91
+ this.requests = new RequestScheduler(resolver.config().maxConcurrentTools);
92
+ this.sharedRequests = new SharedRequestCapacity(resolver.serverState()?.root, resolver.config().maxSharedTools);
87
93
  this.scanner =
88
94
  services.scanner ??
89
95
  new WorkspaceScanner(resolver.config().scanMaxFiles, resolver.config().scanMaxBytes, resolver.config().scanMaxFileBytes);
@@ -159,7 +165,7 @@ export class CoreEngine {
159
165
  const cached = this.#scanCache.get(cacheKey);
160
166
  if (cached !== undefined)
161
167
  return cached;
162
- const index = SymbolIndex.build(files);
168
+ const index = await SymbolIndex.buildAsync(files, controller.signal);
163
169
  const snapshot = {
164
170
  workspaceId,
165
171
  revision,