hoi4-agent-tools 3.0.5 → 3.0.6

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 (45) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/hoi4_agent_tools/core/engine.js +5 -0
  3. package/dist/hoi4_agent_tools/core/idle-cache-lifetime.js +62 -0
  4. package/dist/hoi4_agent_tools/core/scanner.js +34 -7
  5. package/dist/hoi4_agent_tools/event/service.js +12 -1
  6. package/dist/hoi4_agent_tools/gui/assets.js +4 -3
  7. package/dist/hoi4_agent_tools/gui/studio.js +1 -0
  8. package/dist/hoi4_agent_tools/mcp/tools/chaosx.js +9 -0
  9. package/dist/hoi4_agent_tools/mcp/tools/event.js +14 -0
  10. package/dist/hoi4_agent_tools/mcp/tools/focus.js +12 -2
  11. package/dist/hoi4_agent_tools/mcp/tools/gui.js +14 -0
  12. package/dist/hoi4_agent_tools/mcp/tools/map.js +14 -0
  13. package/dist/hoi4_agent_tools/mcp/tools/probability.js +30 -0
  14. package/dist/hoi4_agent_tools/mcp/tools/technology.js +14 -0
  15. package/dist/hoi4_agent_tools/mcp/transports/bounded-stdio.js +21 -0
  16. package/dist/hoi4_agent_tools/probability/service.js +25 -2
  17. package/dist/hoi4_agent_tools/technology/service.js +12 -1
  18. package/dist/hoi4_agent_tools/version.js +1 -1
  19. package/docs/gui.md +1 -1
  20. package/docs/images/readme/chaos-meter-mcp.png +0 -0
  21. package/docs/images/readme/communist-insurgency-mcp.png +0 -0
  22. package/docs/images/readme/settings-advanced-mcp.png +0 -0
  23. package/docs/images/readme/settings-events-mcp.png +0 -0
  24. package/package.json +1 -1
  25. package/schemas/configuration.schema.json +1 -1
  26. package/schemas/continuous-focus-palette.schema.json +1 -1
  27. package/schemas/custom-weighted-pool.schema.json +1 -1
  28. package/schemas/event-feature-manifest.schema.json +1 -1
  29. package/schemas/focus-plan.schema.json +1 -1
  30. package/schemas/focus-planning-sidecar.schema.json +1 -1
  31. package/schemas/gui-animation-source.schema.json +1 -1
  32. package/schemas/gui-helper.schema.json +1 -1
  33. package/schemas/gui-scenario.schema.json +1 -1
  34. package/schemas/map-operation.schema.json +1 -1
  35. package/schemas/operation-result.schema.json +1 -1
  36. package/schemas/probability-analysis-result.schema.json +1 -1
  37. package/schemas/probability-compare-input.schema.json +1 -1
  38. package/schemas/probability-evaluate-input.schema.json +1 -1
  39. package/schemas/probability-inspect-input.schema.json +1 -1
  40. package/schemas/probability-render-input.schema.json +1 -1
  41. package/schemas/probability-scenario-set.schema.json +1 -1
  42. package/schemas/probability-sequence-input.schema.json +1 -1
  43. package/schemas/probability-simulate-input.schema.json +1 -1
  44. package/schemas/probability-sweep-input.schema.json +1 -1
  45. package/server.json +2 -2
package/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@
4
4
 
5
5
  No unreleased changes.
6
6
 
7
+ ## 3.0.6 - 2026-09-01
8
+
9
+ - Prevent long-lived coding-agent tasks from exhausting the host and closing MCP transports. Stdio disconnects now abort in-flight requests, vanilla scan caches are byte-bounded, event and technology histories are reduced, probability results are bounded, and heavy domain caches are released after a concurrent-safe idle window.
10
+ - Improve enlarged HOI4 bitmap text with restrained Lanczos2 interpolation while leaving native atlas coverage unchanged. The renderer still applies no sharpening, thresholding, opacity gain, or reconstructed edges.
11
+ - Refresh the source-backed Chaos Meter, Communist Insurgency, and settings-window render examples with the production GUI route.
12
+
7
13
  ## 3.0.5 - 2026-09-01
8
14
 
9
15
  - Remove enlarged-font edge reconstruction and per-glyph coverage normalization. HOI4 bitmap glyphs now use their decoded atlas samples with nearest-neighbour scaling and integer output placement, without synthetic sharpening, thresholding, gain adjustment, or invented edge pixels.
@@ -210,6 +210,11 @@ export class CoreEngine {
210
210
  flight.controller.abort();
211
211
  }
212
212
  }
213
+ /** Drop completed scan buffers without invalidating revisions or interrupting active scans. */
214
+ releaseScanCaches() {
215
+ this.#scanCache.clear();
216
+ this.scanner.clearCaches();
217
+ }
213
218
  /** Monotonic cache generation used by domain services to invalidate derived snapshots. */
214
219
  generation(workspaceId) {
215
220
  return this.#scanGenerations.get(workspaceId) ?? 0;
@@ -0,0 +1,62 @@
1
+ export const DEFAULT_IDLE_CACHE_RELEASE_MS = 30_000;
2
+ /** Keeps reusable analysis caches alive across an immediate tool sequence, then releases them. */
3
+ export class IdleCacheLifetime {
4
+ releaseCaches;
5
+ idleMs;
6
+ #active = 0;
7
+ #timer;
8
+ constructor(releaseCaches, idleMs = DEFAULT_IDLE_CACHE_RELEASE_MS) {
9
+ this.releaseCaches = releaseCaches;
10
+ this.idleMs = idleMs;
11
+ }
12
+ release() {
13
+ try {
14
+ this.releaseCaches();
15
+ }
16
+ catch (error) {
17
+ process.stderr.write(`${JSON.stringify({
18
+ event: 'cache_release_failed',
19
+ level: 'error',
20
+ message: error instanceof Error ? error.message : String(error),
21
+ })}\n`);
22
+ }
23
+ }
24
+ begin() {
25
+ if (this.#timer !== undefined) {
26
+ clearTimeout(this.#timer);
27
+ this.#timer = undefined;
28
+ }
29
+ this.#active += 1;
30
+ let ended = false;
31
+ return () => {
32
+ if (ended)
33
+ return;
34
+ ended = true;
35
+ this.#active = Math.max(0, this.#active - 1);
36
+ if (this.#active !== 0)
37
+ return;
38
+ this.#timer = setTimeout(() => {
39
+ this.#timer = undefined;
40
+ if (this.#active === 0)
41
+ this.release();
42
+ }, this.idleMs);
43
+ this.#timer.unref();
44
+ };
45
+ }
46
+ async run(operation) {
47
+ const end = this.begin();
48
+ try {
49
+ return await operation();
50
+ }
51
+ finally {
52
+ end();
53
+ }
54
+ }
55
+ clearNow() {
56
+ if (this.#timer !== undefined)
57
+ clearTimeout(this.#timer);
58
+ this.#timer = undefined;
59
+ if (this.#active === 0)
60
+ this.release();
61
+ }
62
+ }
@@ -29,12 +29,15 @@ export class WorkspaceScanner {
29
29
  #sourceCache = new Map();
30
30
  #gameScanCache = new Map();
31
31
  #sourceCacheBytes = 0;
32
+ #gameScanCacheBytes = 0;
32
33
  #sourceCacheMaxBytes;
34
+ #gameScanCacheMaxBytes;
33
35
  constructor(serverMaxFiles = DEFAULT_SCAN_MAX_FILES, serverMaxBytes = DEFAULT_SCAN_MAX_BYTES, serverMaxFileBytes = DEFAULT_SCAN_MAX_FILE_BYTES) {
34
36
  this.serverMaxFiles = serverMaxFiles;
35
37
  this.serverMaxBytes = serverMaxBytes;
36
38
  this.serverMaxFileBytes = serverMaxFileBytes;
37
- this.#sourceCacheMaxBytes = Math.max(serverMaxFileBytes, Math.min(134_217_728, serverMaxBytes));
39
+ this.#sourceCacheMaxBytes = Math.max(1, Math.min(134_217_728, serverMaxBytes));
40
+ this.#gameScanCacheMaxBytes = Math.min(134_217_728, serverMaxBytes);
38
41
  }
39
42
  cacheKey(absolutePath) {
40
43
  const resolved = path.resolve(absolutePath);
@@ -64,15 +67,39 @@ export class WorkspaceScanner {
64
67
  });
65
68
  }
66
69
  retainGameScan(key, files) {
70
+ const bytes = files.reduce((total, file) => total + file.size, 0);
71
+ const previous = this.#gameScanCache.get(key);
72
+ if (previous !== undefined)
73
+ this.#gameScanCacheBytes -= previous.bytes;
67
74
  this.#gameScanCache.delete(key);
68
- this.#gameScanCache.set(key, files.map((file) => ({ ...file })));
69
- while (this.#gameScanCache.size > 8) {
70
- const oldest = this.#gameScanCache.keys().next().value;
75
+ if (bytes > this.#gameScanCacheMaxBytes)
76
+ return;
77
+ this.#gameScanCache.set(key, { files: files.map((file) => ({ ...file })), bytes });
78
+ this.#gameScanCacheBytes += bytes;
79
+ while (this.#gameScanCache.size > 8 || this.#gameScanCacheBytes > this.#gameScanCacheMaxBytes) {
80
+ const oldest = this.#gameScanCache.entries().next().value;
71
81
  if (oldest === undefined)
72
82
  break;
73
- this.#gameScanCache.delete(oldest);
83
+ this.#gameScanCache.delete(oldest[0]);
84
+ this.#gameScanCacheBytes -= oldest[1].bytes;
74
85
  }
75
86
  }
87
+ /** Release retained source buffers after an idle MCP analysis batch. */
88
+ clearCaches() {
89
+ this.#sourceCache.clear();
90
+ this.#sourceCacheBytes = 0;
91
+ this.#gameScanCache.clear();
92
+ this.#gameScanCacheBytes = 0;
93
+ }
94
+ /** Cache accounting used by lifecycle and regression tests. */
95
+ cacheUsage() {
96
+ return {
97
+ sourceBytes: this.#sourceCacheBytes,
98
+ sourceEntries: this.#sourceCache.size,
99
+ gameScanBytes: this.#gameScanCacheBytes,
100
+ gameScanEntries: this.#gameScanCache.size,
101
+ };
102
+ }
76
103
  async scan(workspace, options) {
77
104
  options.signal?.throwIfAborted();
78
105
  const maxFiles = options.maxFiles ?? this.serverMaxFiles;
@@ -102,13 +129,13 @@ export class WorkspaceScanner {
102
129
  if (cachedGameFiles !== undefined && gameScanKey !== undefined) {
103
130
  this.#gameScanCache.delete(gameScanKey);
104
131
  this.#gameScanCache.set(gameScanKey, cachedGameFiles);
105
- enumeratedFiles += cachedGameFiles.length;
132
+ enumeratedFiles += cachedGameFiles.files.length;
106
133
  if (enumeratedFiles > maxFiles)
107
134
  throw new ServiceError('SCAN_FILE_LIMIT', 'Scan exceeds the configured file limit', {
108
135
  files: enumeratedFiles,
109
136
  limit: maxFiles,
110
137
  });
111
- for (const file of cachedGameFiles) {
138
+ for (const file of cachedGameFiles.files) {
112
139
  totalBytes += file.size;
113
140
  if (totalBytes > maxBytes)
114
141
  throw new ServiceError('SCAN_BYTE_LIMIT', 'Scan exceeds the configured byte limit', {
@@ -41,6 +41,10 @@ class BoundedEventFragmentCache {
41
41
  this.#sourceBytes -= oldest[1].sourceBytes;
42
42
  }
43
43
  }
44
+ clear() {
45
+ this.#entries.clear();
46
+ this.#sourceBytes = 0;
47
+ }
44
48
  }
45
49
  function eventGraphCacheKey(workspaceId, projectHelpers, analysisMode = 'full') {
46
50
  return `${workspaceId}\0${projectHelpers ? 'expanded' : 'structural'}\0${analysisMode}`;
@@ -208,7 +212,7 @@ function rememberGraph(state, workspaceId, graph) {
208
212
  const history = state.history.get(workspaceId) ?? new Map();
209
213
  history.delete(graph.revision);
210
214
  history.set(graph.revision, graph);
211
- while (history.size > 8) {
215
+ while (history.size > 2) {
212
216
  const oldest = history.keys().next().value;
213
217
  if (oldest === undefined)
214
218
  break;
@@ -694,6 +698,13 @@ export class EventChainViewer {
694
698
  this.engine = engine;
695
699
  this.#state = sharedState(engine);
696
700
  }
701
+ /** Release full source graphs retained between adjacent MCP calls. */
702
+ clearCaches() {
703
+ this.#state.current.clear();
704
+ this.#state.history.clear();
705
+ this.#state.fragments.clear();
706
+ this.engine.releaseScanCaches();
707
+ }
697
708
  async scan(workspaceId, options = {}) {
698
709
  options.signal?.throwIfAborted();
699
710
  // Authorization is request-scoped. Never let a graph cached by one
@@ -928,11 +928,12 @@ export class GuiAssetCatalog {
928
928
  limitInputPixels: RENDER_MAX_DECODED_PIXELS,
929
929
  });
930
930
  if (targetWidth !== extracted.info.width || targetHeight !== extracted.info.height) {
931
- // BMFont pages are the font. Resize their coverage without interpolation so the
932
- // renderer never invents edge pixels or applies synthetic sharpening.
931
+ // BMFont pages are the font. A restrained two-lobe resample keeps enlarged text from
932
+ // becoming blocky without applying sharpening, thresholding, gain, or reconstructed
933
+ // contours. Native-size coverage remains byte-for-byte unchanged.
933
934
  pipeline = pipeline.resize(targetWidth, targetHeight, {
934
935
  fit: 'fill',
935
- kernel: sharp.kernel.nearest,
936
+ kernel: scale > 1 ? sharp.kernel.lanczos2 : sharp.kernel.nearest,
936
937
  });
937
938
  }
938
939
  return pipeline
@@ -722,6 +722,7 @@ export class ScriptedGuiStudio {
722
722
  /** Release parsed GUI graphs after a bounded batch job has finished using the studio. */
723
723
  clearCaches() {
724
724
  this.#graphCache.clear();
725
+ this.engine.releaseScanCaches();
725
726
  }
726
727
  async scan(workspaceId, principal, signal, languages = ['l_english']) {
727
728
  signal?.throwIfAborted();
@@ -4,6 +4,7 @@ import { z } from 'zod/v4';
4
4
  import { publicArtifactLink } from '../../core/artifacts.js';
5
5
  import { compareCodeUnits, hashCanonical } from '../../core/canonical.js';
6
6
  import { countryAssetDiscoveryPatterns } from '../../core/domain-scan-patterns.js';
7
+ import { IdleCacheLifetime } from '../../core/idle-cache-lifetime.js';
7
8
  import { emptyServiceResult } from '../../core/result.js';
8
9
  import { RenderBudget } from '../../core/render-budget.js';
9
10
  import { workspaceIdSchema } from '../../schemas/common.js';
@@ -335,6 +336,10 @@ async function resolveGuiRevision(engine, context, studio, cache, workspaceId, s
335
336
  export function registerChaosxTools(server, engine, context) {
336
337
  const guiStudio = new ScriptedGuiStudio(engine);
337
338
  const guiRevisionCache = new Map();
339
+ const guiCacheLifetime = new IdleCacheLifetime(() => {
340
+ guiStudio.clearCaches();
341
+ guiRevisionCache.clear();
342
+ });
338
343
  server.registerTool('chaosx.visual_revision', {
339
344
  title: 'Check ChaosX visual revisions',
340
345
  description: 'Private ChaosX cache-coherency endpoint. Computes exact scripted-GUI source revisions without rendering PNG artifacts.',
@@ -343,6 +348,7 @@ export function registerChaosxTools(server, engine, context) {
343
348
  annotations: readOnly,
344
349
  }, async (input, extra) => {
345
350
  const workspaceId = await resolveServerWorkspaceId(engine, context, input.workspaceId, extra.signal);
351
+ const releaseCaches = guiCacheLifetime.begin();
346
352
  try {
347
353
  const progress = progressReporter(extra);
348
354
  await progress.report(0, 2, 'Checking ChaosX scripted-GUI sources');
@@ -372,6 +378,9 @@ export function registerChaosxTools(server, engine, context) {
372
378
  catch (error) {
373
379
  return errorResult(error, workspaceId);
374
380
  }
381
+ finally {
382
+ releaseCaches();
383
+ }
375
384
  });
376
385
  server.registerTool('chaosx.focus_country_assets', {
377
386
  title: 'Render ChaosX focus country assets',
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod/v4';
2
2
  import { publicArtifactLink } from '../../core/artifacts.js';
3
3
  import { compareCodeUnits } from '../../core/canonical.js';
4
+ import { IdleCacheLifetime } from '../../core/idle-cache-lifetime.js';
4
5
  import { emptyServiceResult } from '../../core/result.js';
5
6
  import { EventChainViewer, eventDiagnostics, } from '../../event/service.js';
6
7
  import { eventDirectionSchema, eventGraphReferenceSchema, eventImpactSubjectSchema, eventInspectModeSchema, eventProposedSourceSchema, eventRenderViewSchema, eventSelectorSchema, eventStateSubjectSchema, } from '../../schemas/event.js';
@@ -305,6 +306,7 @@ function compareRequest(input, context, signal) {
305
306
  }
306
307
  export function registerEventTools(server, engine, context) {
307
308
  const viewer = new EventChainViewer(engine);
309
+ const cacheLifetime = new IdleCacheLifetime(() => viewer.clearCaches());
308
310
  server.registerTool('hoi4.event_inspect', {
309
311
  title: 'Inspect event chains',
310
312
  description: 'Scan, find roots, trace, explain paths, inspect state flow, lint, or assess impact. Full source-linked reports are resources.',
@@ -313,6 +315,7 @@ export function registerEventTools(server, engine, context) {
313
315
  annotations: readOnlyEventTool,
314
316
  }, async (input, extra) => {
315
317
  const workspaceId = await resolveServerWorkspaceId(engine, context, input.workspaceId, extra.signal);
318
+ const releaseCaches = cacheLifetime.begin();
316
319
  try {
317
320
  const progress = progressReporter(extra);
318
321
  await progress.report(0, 3, 'Analyzing event chain');
@@ -343,6 +346,9 @@ export function registerEventTools(server, engine, context) {
343
346
  catch (error) {
344
347
  return errorResult(error, workspaceId);
345
348
  }
349
+ finally {
350
+ releaseCaches();
351
+ }
346
352
  });
347
353
  server.registerTool('hoi4.event_render', {
348
354
  title: 'Render event chains',
@@ -352,6 +358,7 @@ export function registerEventTools(server, engine, context) {
352
358
  annotations: readOnlyEventTool,
353
359
  }, async (input, extra) => {
354
360
  const workspaceId = await resolveServerWorkspaceId(engine, context, input.workspaceId, extra.signal);
361
+ const releaseCaches = cacheLifetime.begin();
355
362
  try {
356
363
  const progress = progressReporter(extra);
357
364
  await progress.report(0, 3, 'Rendering event-chain view');
@@ -389,6 +396,9 @@ export function registerEventTools(server, engine, context) {
389
396
  catch (error) {
390
397
  return errorResult(error, workspaceId);
391
398
  }
399
+ finally {
400
+ releaseCaches();
401
+ }
392
402
  });
393
403
  server.registerTool('hoi4.event_compare', {
394
404
  title: 'Compare event chains',
@@ -398,6 +408,7 @@ export function registerEventTools(server, engine, context) {
398
408
  annotations: readOnlyEventTool,
399
409
  }, async (input, extra) => {
400
410
  const workspaceId = await resolveServerWorkspaceId(engine, context, input.workspaceId, extra.signal);
411
+ const releaseCaches = cacheLifetime.begin();
401
412
  try {
402
413
  const progress = progressReporter(extra);
403
414
  await progress.report(0, 3, 'Comparing event-chain graphs');
@@ -463,5 +474,8 @@ export function registerEventTools(server, engine, context) {
463
474
  catch (error) {
464
475
  return errorResult(error, workspaceId);
465
476
  }
477
+ finally {
478
+ releaseCaches();
479
+ }
466
480
  });
467
481
  }
@@ -2,6 +2,7 @@ import { z } from 'zod/v4';
2
2
  import { boundedSourceHashEvidence, publicArtifactLink } from '../../core/artifacts.js';
3
3
  import { compareCodeUnits, canonicalJson, hashCanonical } from '../../core/canonical.js';
4
4
  import { focusDomainScanPatterns } from '../../core/domain-scan-patterns.js';
5
+ import { IdleCacheLifetime } from '../../core/idle-cache-lifetime.js';
5
6
  import { RenderBudget } from '../../core/render-budget.js';
6
7
  import { emptyServiceResult, ServiceError } from '../../core/result.js';
7
8
  import { parseClausewitz } from '../../core/source/index.js';
@@ -604,6 +605,7 @@ function validationFromDiagnostics(diagnostics) {
604
605
  }
605
606
  export function registerFocusTools(server, engine, context) {
606
607
  const workbench = new FocusWorkbench(engine.resolver, engine.transactions, engine.artifacts);
608
+ const cacheLifetime = new IdleCacheLifetime(() => engine.releaseScanCaches());
607
609
  server.registerTool('hoi4.focus_inspect', {
608
610
  title: 'Inspect focus trees',
609
611
  description: 'Inspect national trees or continuous palettes for creation and cleanup, including continuous-focus placement, complete plans, references, diagnostics, and stable layout decisions.',
@@ -613,6 +615,7 @@ export function registerFocusTools(server, engine, context) {
613
615
  }, async (input, extra) => {
614
616
  const { workspaceId: requestedWorkspaceId, relativePath } = input;
615
617
  const workspaceId = await resolveServerWorkspaceForSource(engine, context, requestedWorkspaceId, relativePath, extra.signal);
618
+ const releaseCaches = cacheLifetime.begin();
616
619
  try {
617
620
  const progress = progressReporter(extra);
618
621
  await progress.report(0, 3, 'Building shared workspace index');
@@ -824,6 +827,9 @@ export function registerFocusTools(server, engine, context) {
824
827
  catch (error) {
825
828
  return errorResult(error, workspaceId);
826
829
  }
830
+ finally {
831
+ releaseCaches();
832
+ }
827
833
  });
828
834
  server.registerTool('hoi4.focus_render', {
829
835
  title: 'Render focus review artifacts',
@@ -831,14 +837,14 @@ export function registerFocusTools(server, engine, context) {
831
837
  inputSchema: focusRenderInput,
832
838
  outputSchema: focusRenderOutputSchema,
833
839
  annotations: artifactProducing,
834
- }, (input, extra) => executeFocusVisualTool(engine, workbench, context, input, extra, false));
840
+ }, (input, extra) => cacheLifetime.run(() => executeFocusVisualTool(engine, workbench, context, input, extra, false)));
835
841
  server.registerTool('hoi4.focus_raster', {
836
842
  title: 'Rasterize focus review artifacts',
837
843
  description: 'Produce the high-fidelity focus review with decoded source icons and deterministic PNG output. Use focus_render for the faster structural HTML, SVG, and JSON view.',
838
844
  inputSchema: focusRenderInput,
839
845
  outputSchema: focusRenderOutputSchema,
840
846
  annotations: artifactProducing,
841
- }, (input, extra) => executeFocusVisualTool(engine, workbench, context, input, extra, true));
847
+ }, (input, extra) => cacheLifetime.run(() => executeFocusVisualTool(engine, workbench, context, input, extra, true)));
842
848
  server.registerTool('hoi4.focus_rewrite', {
843
849
  title: 'Create or clean up focus content',
844
850
  description: 'Create or clean up a national tree or continuous palette, validate it, and apply it in one call. Supply a complete plan; set layoutMode compact for automatic arrangement. Existing national trees can omit the plan and use treeId plus layoutMode compact. Set createIfMissing for a new file.',
@@ -853,6 +859,7 @@ export function registerFocusTools(server, engine, context) {
853
859
  }, async (input, extra) => {
854
860
  const { workspaceId: requestedWorkspaceId, relativePath, createIfMissing, horizontalSpacing, verticalSpacing, padding, reviewScale, } = input;
855
861
  const workspaceId = await resolveServerWorkspaceForSource(engine, context, requestedWorkspaceId, relativePath, extra.signal);
862
+ const releaseCaches = cacheLifetime.begin();
856
863
  try {
857
864
  requireServerScope(context, 'hoi4:write');
858
865
  const progress = progressReporter(extra);
@@ -1156,5 +1163,8 @@ export function registerFocusTools(server, engine, context) {
1156
1163
  catch (error) {
1157
1164
  return errorResult(error, workspaceId, autonomousFailureContext(error));
1158
1165
  }
1166
+ finally {
1167
+ releaseCaches();
1168
+ }
1159
1169
  });
1160
1170
  }
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod/v4';
2
2
  import { boundedSourceHashEvidence, publicArtifactLink } from '../../core/artifacts.js';
3
3
  import { canonicalJson, compareCodeUnits, hashCanonical } from '../../core/canonical.js';
4
+ import { IdleCacheLifetime } from '../../core/idle-cache-lifetime.js';
4
5
  import { renderDimensionViolation, RenderBudget, RENDER_MAX_DIMENSION, } from '../../core/render-budget.js';
5
6
  import { emptyServiceResult, ServiceError } from '../../core/result.js';
6
7
  import { SOURCE_MAX_BYTES } from '../../core/source/index.js';
@@ -159,6 +160,7 @@ function validationSummary(validation) {
159
160
  }
160
161
  export function registerGuiTools(server, engine, context) {
161
162
  const studio = new ScriptedGuiStudio(engine);
163
+ const cacheLifetime = new IdleCacheLifetime(() => studio.clearCaches());
162
164
  server.registerTool('hoi4.gui_inspect', {
163
165
  title: 'Inspect scripted GUI',
164
166
  description: 'Inspect GUI sources and scenarios.',
@@ -167,6 +169,7 @@ export function registerGuiTools(server, engine, context) {
167
169
  annotations: artifactProducing,
168
170
  }, async ({ workspaceId: requestedWorkspaceId, windowName, scenario, relatedScenarios, generatedScenarios, }, extra) => {
169
171
  const workspaceId = await resolveServerWorkspaceId(engine, context, requestedWorkspaceId, extra.signal);
172
+ const releaseCaches = cacheLifetime.begin();
170
173
  try {
171
174
  const progress = progressReporter(extra);
172
175
  await progress.report(0, 3, 'Building GUI source graph');
@@ -276,10 +279,14 @@ export function registerGuiTools(server, engine, context) {
276
279
  catch (error) {
277
280
  return errorResult(error, workspaceId);
278
281
  }
282
+ finally {
283
+ releaseCaches();
284
+ }
279
285
  });
280
286
  const render = async (input, extra, code) => {
281
287
  const workspaceId = await resolveServerWorkspaceId(engine, context, input.workspaceId, extra.signal);
282
288
  const { windowName } = input;
289
+ const releaseCaches = cacheLifetime.begin();
283
290
  try {
284
291
  const progress = progressReporter(extra);
285
292
  await progress.report(0, 4, 'Building GUI source graph');
@@ -343,6 +350,9 @@ export function registerGuiTools(server, engine, context) {
343
350
  catch (error) {
344
351
  return errorResult(error, workspaceId);
345
352
  }
353
+ finally {
354
+ releaseCaches();
355
+ }
346
356
  };
347
357
  const renderInput = guiBaseInput
348
358
  .extend({
@@ -488,6 +498,7 @@ export function registerGuiTools(server, engine, context) {
488
498
  }, async (input, extra) => {
489
499
  const workspaceId = await resolveServerWorkspaceId(engine, context, input.workspaceId, extra.signal);
490
500
  const { relativePath, windowName, scenario } = input;
501
+ const releaseCaches = cacheLifetime.begin();
491
502
  try {
492
503
  requireServerScope(context, 'hoi4:write');
493
504
  const progress = progressReporter(extra);
@@ -567,5 +578,8 @@ export function registerGuiTools(server, engine, context) {
567
578
  catch (error) {
568
579
  return errorResult(error, workspaceId, autonomousFailureContext(error));
569
580
  }
581
+ finally {
582
+ releaseCaches();
583
+ }
570
584
  });
571
585
  }
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod/v4';
2
2
  import { boundedSourceHashEvidence, publicArtifactLink, } from '../../core/artifacts.js';
3
3
  import { canonicalJson, hashCanonical } from '../../core/canonical.js';
4
+ import { IdleCacheLifetime } from '../../core/idle-cache-lifetime.js';
4
5
  import { emptyServiceResult } from '../../core/result.js';
5
6
  import { AgentNudger, allocateMapIdentifiersAsync, buildMapCatalog, exportProvinceGeometryRowRuns, lookupMapCoordinate, MAP_PROVINCE_GEOMETRY_SELECTOR_LIMIT, renderMap, searchMapCatalog, } from '../../map/index.js';
6
7
  import { workspaceIdSchema } from '../../schemas/common.js';
@@ -349,6 +350,7 @@ function selectedMapEntities(snapshot, provinceIds, stateIds, regionIds) {
349
350
  }
350
351
  export function registerMapTools(server, engine, context) {
351
352
  const nudger = new AgentNudger(engine);
353
+ const cacheLifetime = new IdleCacheLifetime(() => engine.releaseScanCaches());
352
354
  server.registerTool('hoi4.map_inspect', {
353
355
  title: 'Inspect HOI4 map',
354
356
  description: 'Render, validate, search, and click the complete map.',
@@ -391,6 +393,7 @@ export function registerMapTools(server, engine, context) {
391
393
  annotations: artifactProducing,
392
394
  }, async ({ workspaceId: requestedWorkspaceId, provinceIds, stateIds, regionIds, query, queryLimit, coordinates, includeOverview, allocationRequests, }, extra) => {
393
395
  const workspaceId = await resolveServerWorkspaceId(engine, context, requestedWorkspaceId, extra.signal);
396
+ const releaseCaches = cacheLifetime.begin();
394
397
  try {
395
398
  const progress = progressReporter(extra);
396
399
  await progress.report(0, 3, 'Inspecting and validating the map');
@@ -573,6 +576,9 @@ export function registerMapTools(server, engine, context) {
573
576
  catch (error) {
574
577
  return errorResult(error, workspaceId);
575
578
  }
579
+ finally {
580
+ releaseCaches();
581
+ }
576
582
  });
577
583
  server.registerTool('hoi4.map_render', {
578
584
  title: 'Render map inspection artifacts',
@@ -589,6 +595,7 @@ export function registerMapTools(server, engine, context) {
589
595
  annotations: artifactProducing,
590
596
  }, async ({ workspaceId: requestedWorkspaceId, layer, overlays, scale }, extra) => {
591
597
  const workspaceId = await resolveServerWorkspaceId(engine, context, requestedWorkspaceId, extra.signal);
598
+ const releaseCaches = cacheLifetime.begin();
592
599
  try {
593
600
  const progress = progressReporter(extra);
594
601
  await progress.report(0, 3, 'Rendering offline map layer');
@@ -615,6 +622,9 @@ export function registerMapTools(server, engine, context) {
615
622
  catch (error) {
616
623
  return errorResult(error, workspaceId);
617
624
  }
625
+ finally {
626
+ releaseCaches();
627
+ }
618
628
  });
619
629
  server.registerTool('hoi4.map_rewrite', {
620
630
  title: 'Create or clean up map content',
@@ -635,6 +645,7 @@ export function registerMapTools(server, engine, context) {
635
645
  },
636
646
  }, async ({ workspaceId: requestedWorkspaceId, operations, diffScale }, extra) => {
637
647
  const workspaceId = await resolveServerWorkspaceId(engine, context, requestedWorkspaceId, extra.signal);
648
+ const releaseCaches = cacheLifetime.begin();
638
649
  try {
639
650
  requireServerScope(context, 'hoi4:write');
640
651
  const progress = progressReporter(extra);
@@ -706,5 +717,8 @@ export function registerMapTools(server, engine, context) {
706
717
  catch (error) {
707
718
  return errorResult(error, workspaceId, autonomousFailureContext(error));
708
719
  }
720
+ finally {
721
+ releaseCaches();
722
+ }
709
723
  });
710
724
  }
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod/v4';
2
+ import { IdleCacheLifetime } from '../../core/idle-cache-lifetime.js';
2
3
  import { emptyServiceResult } from '../../core/result.js';
3
4
  import { ProbabilityAnalyzer, } from '../../probability/service.js';
4
5
  import { customWeightedPoolManifestSchema, probabilityAcceptanceBandSchema, probabilityAdapterIdSchema, probabilityDiagnosticThresholdsSchema, probabilityMetricSchema, probabilityOutputSchema, probabilityRenderFilterSchema, probabilityScenarioSetSchema, probabilitySamplingMethodSchema, probabilitySourceSchema, } from '../../schemas/probability.js';
@@ -303,6 +304,7 @@ function analysisServiceResult(workspaceId, result) {
303
304
  }
304
305
  export function registerProbabilityTools(server, engine, context) {
305
306
  const analyzer = new ProbabilityAnalyzer(engine);
307
+ const cacheLifetime = new IdleCacheLifetime(() => analyzer.clearCaches());
306
308
  server.registerTool('hoi4.probability_inspect', {
307
309
  title: 'Inspect AI and MTTH weighted logic',
308
310
  description: 'Discover weighted adapters, candidates, provenance, and required inputs.',
@@ -312,6 +314,7 @@ export function registerProbabilityTools(server, engine, context) {
312
314
  }, async (input, extra) => {
313
315
  const progress = progressReporter(extra);
314
316
  const base = await requestContext(engine, context, input.workspaceId, input.refresh, progress.signal);
317
+ const releaseCaches = cacheLifetime.begin();
315
318
  try {
316
319
  await progress.report(0, 2, 'Inspecting weighted source');
317
320
  const inspected = await analyzer.inspect(base, input.adapter, input.source, input.candidatePool, input.customPoolManifest);
@@ -364,6 +367,9 @@ export function registerProbabilityTools(server, engine, context) {
364
367
  catch (error) {
365
368
  return errorResult(error, base.workspaceId);
366
369
  }
370
+ finally {
371
+ releaseCaches();
372
+ }
367
373
  });
368
374
  server.registerTool('hoi4.probability_evaluate', {
369
375
  title: 'Evaluate AI and MTTH scenarios',
@@ -374,6 +380,7 @@ export function registerProbabilityTools(server, engine, context) {
374
380
  }, async (input, extra) => {
375
381
  const progress = progressReporter(extra);
376
382
  const base = await requestContext(engine, context, input.workspaceId, input.refresh, progress.signal);
383
+ const releaseCaches = cacheLifetime.begin();
377
384
  try {
378
385
  await progress.report(0, 3, 'Scanning and evaluating weighted source');
379
386
  const request = {
@@ -388,6 +395,9 @@ export function registerProbabilityTools(server, engine, context) {
388
395
  catch (error) {
389
396
  return errorResult(error, base.workspaceId);
390
397
  }
398
+ finally {
399
+ releaseCaches();
400
+ }
391
401
  });
392
402
  server.registerTool('hoi4.probability_sweep', {
393
403
  title: 'Sweep AI and MTTH parameters',
@@ -398,6 +408,7 @@ export function registerProbabilityTools(server, engine, context) {
398
408
  }, async (input, extra) => {
399
409
  const progress = progressReporter(extra);
400
410
  const base = await requestContext(engine, context, input.workspaceId, input.refresh, progress.signal);
411
+ const releaseCaches = cacheLifetime.begin();
401
412
  try {
402
413
  await progress.report(0, 3, 'Evaluating parameter sweep');
403
414
  const request = {
@@ -412,6 +423,9 @@ export function registerProbabilityTools(server, engine, context) {
412
423
  catch (error) {
413
424
  return errorResult(error, base.workspaceId);
414
425
  }
426
+ finally {
427
+ releaseCaches();
428
+ }
415
429
  });
416
430
  server.registerTool('hoi4.probability_simulate', {
417
431
  title: 'Simulate uncertain weighted scenarios',
@@ -422,6 +436,7 @@ export function registerProbabilityTools(server, engine, context) {
422
436
  }, async (input, extra) => {
423
437
  const progress = progressReporter(extra);
424
438
  const base = await requestContext(engine, context, input.workspaceId, input.refresh, progress.signal);
439
+ const releaseCaches = cacheLifetime.begin();
425
440
  try {
426
441
  await progress.report(0, input.samples, 'Sampling uncertain scenarios');
427
442
  const request = {
@@ -436,6 +451,9 @@ export function registerProbabilityTools(server, engine, context) {
436
451
  catch (error) {
437
452
  return errorResult(error, base.workspaceId);
438
453
  }
454
+ finally {
455
+ releaseCaches();
456
+ }
439
457
  });
440
458
  server.registerTool('hoi4.probability_sequence', {
441
459
  title: 'Analyze a declared weighted sequence',
@@ -446,6 +464,7 @@ export function registerProbabilityTools(server, engine, context) {
446
464
  }, async (input, extra) => {
447
465
  const progress = progressReporter(extra);
448
466
  const base = await requestContext(engine, context, input.workspaceId, undefined, progress.signal);
467
+ const releaseCaches = cacheLifetime.begin();
449
468
  try {
450
469
  await progress.report(0, input.maxSteps, 'Analyzing declared sequence');
451
470
  const request = {
@@ -460,6 +479,9 @@ export function registerProbabilityTools(server, engine, context) {
460
479
  catch (error) {
461
480
  return errorResult(error, base.workspaceId);
462
481
  }
482
+ finally {
483
+ releaseCaches();
484
+ }
463
485
  });
464
486
  server.registerTool('hoi4.probability_compare', {
465
487
  title: 'Compare weighted source patches',
@@ -470,6 +492,7 @@ export function registerProbabilityTools(server, engine, context) {
470
492
  }, async (input, extra) => {
471
493
  const progress = progressReporter(extra);
472
494
  const base = await requestContext(engine, context, input.workspaceId, input.refresh, progress.signal);
495
+ const releaseCaches = cacheLifetime.begin();
473
496
  try {
474
497
  await progress.report(0, 4, 'Comparing weighted source');
475
498
  const request = {
@@ -484,6 +507,9 @@ export function registerProbabilityTools(server, engine, context) {
484
507
  catch (error) {
485
508
  return errorResult(error, base.workspaceId);
486
509
  }
510
+ finally {
511
+ releaseCaches();
512
+ }
487
513
  });
488
514
  server.registerTool('hoi4.probability_render', {
489
515
  title: 'Render AI and MTTH analysis',
@@ -494,6 +520,7 @@ export function registerProbabilityTools(server, engine, context) {
494
520
  }, async (input, extra) => {
495
521
  const progress = progressReporter(extra);
496
522
  const base = await requestContext(engine, context, input.workspaceId, undefined, progress.signal);
523
+ const releaseCaches = cacheLifetime.begin();
497
524
  try {
498
525
  await progress.report(0, input.outputs.length, 'Rendering analysis resources');
499
526
  const request = {
@@ -509,6 +536,9 @@ export function registerProbabilityTools(server, engine, context) {
509
536
  catch (error) {
510
537
  return errorResult(error, base.workspaceId);
511
538
  }
539
+ finally {
540
+ releaseCaches();
541
+ }
512
542
  });
513
543
  server.registerPrompt('hoi4.probability_analysis', {
514
544
  title: 'Analyze HOI4 weighted logic',
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod/v4';
2
2
  import { publicArtifactLink } from '../../core/artifacts.js';
3
3
  import { compareCodeUnits, hashCanonical } from '../../core/canonical.js';
4
+ import { IdleCacheLifetime } from '../../core/idle-cache-lifetime.js';
4
5
  import { emptyServiceResult } from '../../core/result.js';
5
6
  import { workspaceIdSchema } from '../../schemas/common.js';
6
7
  import { technologyDefectClassSchema, technologyDirectionSchema, technologyGraphReferenceSchema, technologyIdSchema, technologyImpactSchema, technologyProposedSourceSchema, technologyRenderViewSchema, technologyUnlockKindSchema, } from '../../schemas/technology.js';
@@ -238,6 +239,7 @@ function analysisRequest(input, workspaceId, context, signal) {
238
239
  }
239
240
  export function registerTechnologyTools(server, engine, context) {
240
241
  const viewer = new TechnologyTreeViewer(engine);
242
+ const cacheLifetime = new IdleCacheLifetime(() => viewer.clearCaches());
241
243
  server.registerTool('hoi4.tech_inspect', {
242
244
  title: 'Inspect technology trees',
243
245
  description: 'Scan, discover folders, trace, explain, inspect unlocks or bonuses, lint, and assess impact.',
@@ -246,6 +248,7 @@ export function registerTechnologyTools(server, engine, context) {
246
248
  annotations: readOnly,
247
249
  }, async (input, extra) => {
248
250
  const workspaceId = await resolveServerWorkspaceId(engine, context, input.workspaceId, extra.signal);
251
+ const releaseCaches = cacheLifetime.begin();
249
252
  try {
250
253
  const progress = progressReporter(extra);
251
254
  await progress.report(0, 2, 'Analyzing technologies');
@@ -266,6 +269,9 @@ export function registerTechnologyTools(server, engine, context) {
266
269
  catch (error) {
267
270
  return errorResult(error, workspaceId);
268
271
  }
272
+ finally {
273
+ releaseCaches();
274
+ }
269
275
  });
270
276
  server.registerTool('hoi4.tech_render', {
271
277
  title: 'Render technology trees',
@@ -275,6 +281,7 @@ export function registerTechnologyTools(server, engine, context) {
275
281
  annotations: readOnly,
276
282
  }, async (input, extra) => {
277
283
  const workspaceId = await resolveServerWorkspaceId(engine, context, input.workspaceId, extra.signal);
284
+ const releaseCaches = cacheLifetime.begin();
278
285
  try {
279
286
  const progress = progressReporter(extra);
280
287
  const request = {
@@ -310,6 +317,9 @@ export function registerTechnologyTools(server, engine, context) {
310
317
  catch (error) {
311
318
  return errorResult(error, workspaceId);
312
319
  }
320
+ finally {
321
+ releaseCaches();
322
+ }
313
323
  });
314
324
  server.registerTool('hoi4.tech_compare', {
315
325
  title: 'Compare technology trees',
@@ -319,6 +329,7 @@ export function registerTechnologyTools(server, engine, context) {
319
329
  annotations: readOnly,
320
330
  }, async (input, extra) => {
321
331
  const workspaceId = await resolveServerWorkspaceId(engine, context, input.workspaceId, extra.signal);
332
+ const releaseCaches = cacheLifetime.begin();
322
333
  try {
323
334
  const progress = progressReporter(extra);
324
335
  const request = {
@@ -368,5 +379,8 @@ export function registerTechnologyTools(server, engine, context) {
368
379
  catch (error) {
369
380
  return errorResult(error, workspaceId);
370
381
  }
382
+ finally {
383
+ releaseCaches();
384
+ }
371
385
  });
372
386
  }
@@ -99,6 +99,19 @@ export class BoundedStdioServerTransport {
99
99
  handleInputError = (error) => {
100
100
  this.onerror(error);
101
101
  };
102
+ handleInputEnd = () => {
103
+ this.finish();
104
+ };
105
+ handleInputClose = () => {
106
+ this.finish();
107
+ };
108
+ handleOutputError = (error) => {
109
+ this.onerror(error);
110
+ this.finish(true);
111
+ };
112
+ handleOutputClose = () => {
113
+ this.finish(true);
114
+ };
102
115
  processCompleteFrame() {
103
116
  let line;
104
117
  try {
@@ -134,6 +147,10 @@ export class BoundedStdioServerTransport {
134
147
  this.closed = true;
135
148
  this.input.off('data', this.handleData);
136
149
  this.input.off('error', this.handleInputError);
150
+ this.input.off('end', this.handleInputEnd);
151
+ this.input.off('close', this.handleInputClose);
152
+ this.output.off('error', this.handleOutputError);
153
+ this.output.off('close', this.handleOutputClose);
137
154
  if (destroyInput)
138
155
  this.input.destroy();
139
156
  else if (this.input.listenerCount('data') === 0)
@@ -147,6 +164,10 @@ export class BoundedStdioServerTransport {
147
164
  this.started = true;
148
165
  this.input.on('data', this.handleData);
149
166
  this.input.on('error', this.handleInputError);
167
+ this.input.once('end', this.handleInputEnd);
168
+ this.input.once('close', this.handleInputClose);
169
+ this.output.on('error', this.handleOutputError);
170
+ this.output.once('close', this.handleOutputClose);
150
171
  return Promise.resolve();
151
172
  }
152
173
  close() {
@@ -17,6 +17,7 @@ import { Rational } from './rational.js';
17
17
  import { discoverWeightedSurface, inventoryWeightedSurfaces, } from './source-analysis.js';
18
18
  import { sweepSurface } from './sweep.js';
19
19
  const states = new WeakMap();
20
+ const ANALYSIS_CACHE_ENTRIES = 16;
20
21
  function resolveSourceBackedAdapter(snapshot, source, candidatePool, requested) {
21
22
  if (requested !== undefined && requested !== 'custom_weighted_pool') {
22
23
  try {
@@ -62,6 +63,23 @@ function analyzerState(engine) {
62
63
  }
63
64
  return state;
64
65
  }
66
+ function rememberAnalysis(state, result) {
67
+ const replacedByCacheKey = state.byCacheKey.get(result.metadata.cacheKey);
68
+ if (replacedByCacheKey !== undefined)
69
+ state.byId.delete(replacedByCacheKey.analysisId);
70
+ state.byId.delete(result.analysisId);
71
+ state.byCacheKey.delete(result.metadata.cacheKey);
72
+ state.byId.set(result.analysisId, result);
73
+ state.byCacheKey.set(result.metadata.cacheKey, result);
74
+ while (state.byId.size > ANALYSIS_CACHE_ENTRIES) {
75
+ const oldest = state.byId.entries().next().value;
76
+ if (oldest === undefined)
77
+ break;
78
+ state.byId.delete(oldest[0]);
79
+ if (state.byCacheKey.get(oldest[1].metadata.cacheKey) === oldest[1])
80
+ state.byCacheKey.delete(oldest[1].metadata.cacheKey);
81
+ }
82
+ }
65
83
  const DEFAULT_DIAGNOSTIC_THRESHOLDS = {
66
84
  dominantProbability: 0.9,
67
85
  dominantScenarioPrevalence: 0.8,
@@ -686,6 +704,12 @@ export class ProbabilityAnalyzer {
686
704
  this.engine = engine;
687
705
  this.state = analyzerState(engine);
688
706
  }
707
+ /** Release source snapshots and analysis results after an idle MCP batch. */
708
+ clearCaches() {
709
+ this.state.byId.clear();
710
+ this.state.byCacheKey.clear();
711
+ this.engine.releaseScanCaches();
712
+ }
689
713
  async scan(context, sourcePaths = [], emptyScope = 'domain') {
690
714
  if (context.refresh === true)
691
715
  this.engine.invalidate(context.workspaceId);
@@ -880,8 +904,7 @@ export class ProbabilityAnalyzer {
880
904
  const stored = await this.engine.artifacts.withAtomicChunkedWrites(workspace, writes, (artifacts) => Promise.resolve([...artifacts]), signal);
881
905
  const complete = { ...result, resources: stored.map(publicArtifactLink) };
882
906
  if (complete.operation !== 'render') {
883
- this.state.byId.set(complete.analysisId, complete);
884
- this.state.byCacheKey.set(complete.metadata.cacheKey, complete);
907
+ rememberAnalysis(this.state, complete);
885
908
  }
886
909
  return complete;
887
910
  }
@@ -19,7 +19,7 @@ const PROPOSED_SOURCE_FILES = 128;
19
19
  const PROPOSED_SOURCE_BYTES = 67_108_864;
20
20
  const GRAPH_ARTIFACT_BYTES = 134_217_728;
21
21
  const GRAPH_ARTIFACT_CHUNKS = 2_048;
22
- const HISTORY_ENTRIES = 12;
22
+ const HISTORY_ENTRIES = 2;
23
23
  const FOCUSED_RENDER_LIMIT = 32;
24
24
  class BoundedTechnologyFragmentCache {
25
25
  #values = new Map();
@@ -47,6 +47,10 @@ class BoundedTechnologyFragmentCache {
47
47
  this.#bytes -= oldest[1].bytes;
48
48
  }
49
49
  }
50
+ clear() {
51
+ this.#values.clear();
52
+ this.#bytes = 0;
53
+ }
50
54
  }
51
55
  const sharedTechnologyStates = new WeakMap();
52
56
  function sharedState(engine) {
@@ -429,6 +433,13 @@ export class TechnologyTreeViewer {
429
433
  this.engine = engine;
430
434
  this.#state = sharedState(engine);
431
435
  }
436
+ /** Release full source graphs retained between adjacent MCP calls. */
437
+ clearCaches() {
438
+ this.#state.current.clear();
439
+ this.#state.history.clear();
440
+ this.#state.fragments.clear();
441
+ this.engine.releaseScanCaches();
442
+ }
432
443
  async scan(workspaceId, options = {}) {
433
444
  options.signal?.throwIfAborted();
434
445
  const workspace = this.engine.resolver.get(workspaceId, options.principal);
@@ -1,6 +1,6 @@
1
1
  import { LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/sdk/types.js';
2
2
  export const PACKAGE_NAME = 'hoi4-agent-tools';
3
- export const PACKAGE_VERSION = '3.0.5';
3
+ export const PACKAGE_VERSION = '3.0.6';
4
4
  export const MCP_PROTOCOL_VERSION = LATEST_PROTOCOL_VERSION;
5
5
  export const CONFIG_VERSION = 1;
6
6
  export const TRANSACTION_VERSION = 1;
package/docs/gui.md CHANGED
@@ -35,7 +35,7 @@ The renderer resolves sprites and fonts through the complete mod, dependency, an
35
35
 
36
36
  Bitmap text uses the language-appropriate HOI4 font definition and its real glyph atlas. Header, serif, compact, typewriter, black, inverted, and other declared faces keep their own glyph shapes, native metrics, face colour, border colour, language override, and font-specific `textcolors` palette. The renderer reads BMFont channel roles, separates face and outline coverage, and recognizes both vanilla black-on-white masks and conventional white-on-transparent custom masks. HOI4 localisation colour runs such as `§Y` and `§R` therefore colour the glyph face while retaining its sharp antialiasing and declared border instead of flattening the glyph into a blurred monochrome mask or switching to the tool font. The default language uses the base font rather than an unrelated language override, and the renderer recognizes installed-game atlases whose filename omits a page suffix declared by the matching `.fnt` file.
37
37
 
38
- `uiScale` is the in-game UI scale, not an image zoom. Use `1` for the normal 1920×1080 layout. Bitmap-font glyphs come directly from the real atlas without a sharpening, threshold, or reconstructed-edge pass. When a larger output is requested, coverage uses nearest-neighbour sampling so the renderer does not invent pixels. Bitmap placement is snapped to the output pixel grid, and bitmap colours use alpha masks, so native, fractional-scale, local, and VPS renders remain deterministic.
38
+ `uiScale` is the in-game UI scale, not an image zoom. Use `1` for the normal 1920×1080 layout. Bitmap-font glyphs come directly from the real atlas without a sharpening, threshold, opacity-gain, or reconstructed-edge pass. Native-size coverage is unchanged. Enlarged coverage uses restrained Lanczos2 interpolation to avoid blocky pixel replication without running a sharpening filter. Downscaling uses nearest-neighbour sampling. Bitmap placement is snapped to the output pixel grid, and bitmap colours use alpha masks, so native, fractional-scale, local, and VPS renders remain deterministic.
39
39
 
40
40
  Layout follows Clausewitz orientation and element-origin anchors, including `CENTER_LEFT`, `CENTER_RIGHT`, `CENTER_UP`, `CENTER_DOWN`, `origo`, `centerposition`, inherited container coordinate origins, local scale, and both `%` and `%%` relative dimensions. Text boxes use native font metrics together with `maxWidth`, `maxHeight`, alignment, wrapping, and `fixedsize` clipping. Cornered tile sprites use fixed nine-slice borders and optional center tiling; progress bars composite their filled and background textures; masked shields composite their background and mask textures.
41
41
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hoi4-agent-tools",
3
3
  "mcpName": "io.github.klimPaskov/hoi4-agent-tools",
4
- "version": "3.0.5",
4
+ "version": "3.0.6",
5
5
  "description": "Analyze HOI4 events, tech trees, AI weights, and MTTH; build focus trees, GUIs, and maps.",
6
6
  "type": "module",
7
7
  "license": "Apache-2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.5/schemas/configuration.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.6/schemas/configuration.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.5/schemas/continuous-focus-palette.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.6/schemas/continuous-focus-palette.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.5/schemas/custom-weighted-pool.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.6/schemas/custom-weighted-pool.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.5/schemas/event-feature-manifest.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.6/schemas/event-feature-manifest.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.5/schemas/focus-plan.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.6/schemas/focus-plan.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.5/schemas/focus-planning-sidecar.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.6/schemas/focus-planning-sidecar.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.5/schemas/gui-animation-source.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.6/schemas/gui-animation-source.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.5/schemas/gui-helper.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.6/schemas/gui-helper.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.5/schemas/gui-scenario.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.6/schemas/gui-scenario.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.5/schemas/map-operation.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.6/schemas/map-operation.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "oneOf": [
5
5
  {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.5/schemas/operation-result.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.6/schemas/operation-result.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.5/schemas/probability-analysis-result.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.6/schemas/probability-analysis-result.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.5/schemas/probability-compare-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.6/schemas/probability-compare-input.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.5/schemas/probability-evaluate-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.6/schemas/probability-evaluate-input.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.5/schemas/probability-inspect-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.6/schemas/probability-inspect-input.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.5/schemas/probability-render-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.6/schemas/probability-render-input.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.5/schemas/probability-scenario-set.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.6/schemas/probability-scenario-set.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.5/schemas/probability-sequence-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.6/schemas/probability-sequence-input.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.5/schemas/probability-simulate-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.6/schemas/probability-simulate-input.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.5/schemas/probability-sweep-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.6/schemas/probability-sweep-input.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
package/server.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "name": "io.github.klimPaskov/hoi4-agent-tools",
4
4
  "title": "HOI4 Agent Tools",
5
5
  "description": "Analyze HOI4 events, tech trees, AI weights, and MTTH; build focus trees, GUIs, and maps.",
6
- "version": "3.0.5",
6
+ "version": "3.0.6",
7
7
  "repository": {
8
8
  "url": "https://github.com/klimPaskov/hoi4-agent-tools",
9
9
  "source": "github"
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "registryType": "npm",
15
15
  "identifier": "hoi4-agent-tools",
16
- "version": "3.0.5",
16
+ "version": "3.0.6",
17
17
  "transport": {
18
18
  "type": "stdio"
19
19
  }