hoi4-agent-tools 3.8.1 → 3.10.0
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/CHANGELOG.md +24 -1
- package/README.md +1 -1
- package/dist/bin/http.js +2 -0
- package/dist/bin/setup.js +5 -2
- package/dist/bin/stdio.js +29 -1
- package/dist/hoi4_agent_tools/core/configuration.js +8 -0
- package/dist/hoi4_agent_tools/core/engine.js +27 -8
- package/dist/hoi4_agent_tools/core/index.js +45 -20
- package/dist/hoi4_agent_tools/core/job-worker-host.js +40 -2
- package/dist/hoi4_agent_tools/core/job-worker.js +14 -0
- package/dist/hoi4_agent_tools/core/scenario-model.js +1 -1
- package/dist/hoi4_agent_tools/core/server-registry.js +138 -0
- package/dist/hoi4_agent_tools/core/server-status-resource.js +22 -0
- package/dist/hoi4_agent_tools/core/source/cache.js +89 -20
- package/dist/hoi4_agent_tools/event/service.js +7 -0
- package/dist/hoi4_agent_tools/gui/renderer.js +8 -2
- package/dist/hoi4_agent_tools/gui/scenario-generator.js +56 -0
- package/dist/hoi4_agent_tools/gui/scenario.js +2 -0
- package/dist/hoi4_agent_tools/gui/studio.js +31 -12
- package/dist/hoi4_agent_tools/gui/validators.js +22 -0
- package/dist/hoi4_agent_tools/mcp/resources/register-modern.js +13 -1
- package/dist/hoi4_agent_tools/mcp/resources/register.js +6 -0
- package/dist/hoi4_agent_tools/mcp/tools/reference.js +1 -1
- package/dist/hoi4_agent_tools/mcp/tools/technology.js +5 -2
- package/dist/hoi4_agent_tools/mcp/transports/negotiated-stdio.js +9 -0
- package/dist/hoi4_agent_tools/mcp/transports/stdio-lifetime.js +79 -0
- package/dist/hoi4_agent_tools/reference/event-popups.js +87 -0
- package/dist/hoi4_agent_tools/reference/operations.js +151 -1
- package/dist/hoi4_agent_tools/reference/player-text.js +158 -0
- package/dist/hoi4_agent_tools/reference/script-validation.js +315 -33
- package/dist/hoi4_agent_tools/reference/source-locate.js +1 -1
- package/dist/hoi4_agent_tools/schemas/gui-requests.js +18 -4
- package/dist/hoi4_agent_tools/schemas/script-validation.js +40 -7
- package/dist/hoi4_agent_tools/schemas/technology.js +4 -2
- package/dist/hoi4_agent_tools/technology/service.js +7 -0
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/gui.md +25 -0
- package/docs/jobs.md +8 -0
- package/docs/reference.md +38 -0
- package/docs/setup.md +11 -0
- package/docs/tools.md +1 -1
- package/npm-shrinkwrap.json +12 -8
- package/package.json +1 -1
- package/schemas/configuration.schema.json +163 -128
- package/schemas/continuous-focus-palette.schema.json +1 -1
- package/schemas/custom-weighted-pool.schema.json +1 -1
- package/schemas/decision-inspect-input.schema.json +1 -1
- package/schemas/event-feature-manifest.schema.json +1 -1
- package/schemas/focus-plan.schema.json +1 -1
- package/schemas/focus-planning-sidecar.schema.json +1 -1
- package/schemas/gui-animation-source.schema.json +1 -1
- package/schemas/gui-helper.schema.json +1 -1
- package/schemas/gui-scenario.schema.json +38 -22
- package/schemas/helper-expansion-request.schema.json +1 -1
- package/schemas/helper-expansion-summary.schema.json +1 -1
- package/schemas/impact-inspect-input.schema.json +1 -1
- package/schemas/map-operation.schema.json +1 -1
- package/schemas/mechanic-test-input.schema.json +1 -1
- package/schemas/operation-result.schema.json +1 -1
- package/schemas/package-check-input.schema.json +1 -1
- package/schemas/package-manifest.schema.json +1 -1
- package/schemas/probability-analysis-result.schema.json +1 -1
- package/schemas/probability-compare-input.schema.json +1 -1
- package/schemas/probability-evaluate-input.schema.json +1 -1
- package/schemas/probability-inspect-input.schema.json +1 -1
- package/schemas/probability-render-input.schema.json +1 -1
- package/schemas/probability-scenario-set.schema.json +1 -1
- package/schemas/probability-sequence-input.schema.json +1 -1
- package/schemas/probability-simulate-input.schema.json +1 -1
- package/schemas/probability-sweep-input.schema.json +1 -1
- package/schemas/reference-context-input.schema.json +1 -1
- package/schemas/reference-read-input.schema.json +1 -1
- package/schemas/reference-search-input.schema.json +1 -1
- package/schemas/scenario-suite.schema.json +1 -1
- package/schemas/scenario-test-input.schema.json +1 -1
- package/schemas/script-validate-input.schema.json +60 -36
- package/schemas/shared-scenario.schema.json +1 -1
- package/schemas/source-lookup-input.schema.json +1 -1
- package/server.json +2 -2
|
@@ -27,29 +27,50 @@ function decodedDocument(encoded, expected) {
|
|
|
27
27
|
throw new Error('Persistent source document does not match its source identity');
|
|
28
28
|
return value;
|
|
29
29
|
}
|
|
30
|
-
/**
|
|
30
|
+
/** Puts between halvings of every entry's hit count, so entries no longer read become evictable. */
|
|
31
|
+
const HIT_AGING_INTERVAL = 4096;
|
|
32
|
+
/** Keys of documents turned away from a full cache, remembered so a repeat can be admitted. */
|
|
33
|
+
const DOORKEEPER_KEYS = 16_384;
|
|
34
|
+
/**
|
|
35
|
+
* Process-local parsed facts; serialized entries cannot be mutated by their consumers.
|
|
36
|
+
*
|
|
37
|
+
* A workspace scan often parses more source than the cache holds. Plain LRU then evicts
|
|
38
|
+
* every document before its next use and pays serialization for nothing. A full cache
|
|
39
|
+
* therefore turns a document away the first time, remembering only its key, and admits it
|
|
40
|
+
* on a later miss by displacing entries that were never read; a rejected document is not
|
|
41
|
+
* serialized. Repeated scans keep what fits and hit it. Hit counts age, so documents that
|
|
42
|
+
* stop being read become evictable again.
|
|
43
|
+
*/
|
|
31
44
|
export class SourceDocumentCache {
|
|
32
45
|
maxBytes;
|
|
33
46
|
#entries = new Map();
|
|
34
47
|
#bytes = 0;
|
|
48
|
+
/** Bytes held by entries without hits, which a new document may displace. */
|
|
49
|
+
#coldBytes = 0;
|
|
50
|
+
#puts = 0;
|
|
35
51
|
#hits = 0;
|
|
36
52
|
#misses = 0;
|
|
37
53
|
#evictions = 0;
|
|
54
|
+
#rejections = 0;
|
|
55
|
+
#doorkeeper = new Set();
|
|
38
56
|
constructor(maxBytes = 64 * 1024 * 1024) {
|
|
39
57
|
this.maxBytes = maxBytes;
|
|
40
58
|
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0)
|
|
41
59
|
throw new RangeError('Source cache size must be a non-negative integer');
|
|
42
60
|
}
|
|
43
61
|
get(key) {
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
62
|
+
const entry = this.#entries.get(key);
|
|
63
|
+
if (entry === undefined) {
|
|
46
64
|
this.#misses += 1;
|
|
47
65
|
return undefined;
|
|
48
66
|
}
|
|
49
67
|
this.#hits += 1;
|
|
68
|
+
if (entry.hits === 0)
|
|
69
|
+
this.#coldBytes -= entrySize(key, entry.encoded);
|
|
70
|
+
entry.hits += 1;
|
|
50
71
|
this.#entries.delete(key);
|
|
51
|
-
this.#entries.set(key,
|
|
52
|
-
return decodedDocument(encoded);
|
|
72
|
+
this.#entries.set(key, entry);
|
|
73
|
+
return decodedDocument(entry.encoded);
|
|
53
74
|
}
|
|
54
75
|
/** Availability check without deserialization or cache-hit accounting. */
|
|
55
76
|
has(key) {
|
|
@@ -61,8 +82,19 @@ export class SourceDocumentCache {
|
|
|
61
82
|
const estimatedBytes = document.bytes.length * 12 + document.tokens.length * 512 + key.length * 2 + 128;
|
|
62
83
|
if (this.maxBytes === 0 || estimatedBytes > this.maxBytes)
|
|
63
84
|
return;
|
|
64
|
-
|
|
65
|
-
this.
|
|
85
|
+
this.#age();
|
|
86
|
+
const previous = this.#entries.get(key);
|
|
87
|
+
const reusable = previous === undefined ? 0 : entrySize(key, previous.encoded);
|
|
88
|
+
const free = this.maxBytes - this.#bytes + reusable;
|
|
89
|
+
if (estimatedBytes > free) {
|
|
90
|
+
// Only a document missed before may displace entries, and only entries never read.
|
|
91
|
+
if (!this.#doorkeeper.delete(key) || estimatedBytes > free + this.#coldBytes) {
|
|
92
|
+
this.#remember(key);
|
|
93
|
+
this.#rejections += 1;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
this.retain(key, serialize(document));
|
|
66
98
|
}
|
|
67
99
|
/** Import one authenticated same-runtime entry without counting it as a process-local hit. */
|
|
68
100
|
importEncoded(key, encoded, expected) {
|
|
@@ -70,42 +102,79 @@ export class SourceDocumentCache {
|
|
|
70
102
|
this.retain(key, Buffer.from(encoded));
|
|
71
103
|
}
|
|
72
104
|
exportEncoded(key) {
|
|
73
|
-
const
|
|
74
|
-
return
|
|
105
|
+
const entry = this.#entries.get(key);
|
|
106
|
+
return entry === undefined ? undefined : Buffer.from(entry.encoded);
|
|
75
107
|
}
|
|
76
108
|
retain(key, encoded) {
|
|
77
|
-
const size =
|
|
109
|
+
const size = entrySize(key, encoded);
|
|
78
110
|
if (size > this.maxBytes)
|
|
79
111
|
return;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
while (this.#bytes + size > this.maxBytes) {
|
|
85
|
-
const oldest = this.#entries.entries().next().value;
|
|
86
|
-
if (oldest === undefined)
|
|
112
|
+
this.#remove(key);
|
|
113
|
+
// Displace the oldest entries that were never read again; read entries stay.
|
|
114
|
+
for (const [oldKey, entry] of this.#entries) {
|
|
115
|
+
if (this.#bytes + size <= this.maxBytes)
|
|
87
116
|
break;
|
|
88
|
-
|
|
89
|
-
|
|
117
|
+
if (entry.hits > 0)
|
|
118
|
+
continue;
|
|
119
|
+
this.#remove(oldKey);
|
|
90
120
|
this.#evictions += 1;
|
|
91
121
|
}
|
|
92
|
-
this.#
|
|
122
|
+
if (this.#bytes + size > this.maxBytes) {
|
|
123
|
+
this.#rejections += 1;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
this.#entries.set(key, { encoded, hits: 0 });
|
|
93
127
|
this.#bytes += size;
|
|
128
|
+
this.#coldBytes += size;
|
|
129
|
+
}
|
|
130
|
+
#remember(key) {
|
|
131
|
+
this.#doorkeeper.delete(key);
|
|
132
|
+
this.#doorkeeper.add(key);
|
|
133
|
+
if (this.#doorkeeper.size > DOORKEEPER_KEYS)
|
|
134
|
+
this.#doorkeeper.delete(this.#doorkeeper.values().next().value);
|
|
135
|
+
}
|
|
136
|
+
#remove(key) {
|
|
137
|
+
const entry = this.#entries.get(key);
|
|
138
|
+
if (entry === undefined)
|
|
139
|
+
return;
|
|
140
|
+
const size = entrySize(key, entry.encoded);
|
|
141
|
+
this.#entries.delete(key);
|
|
142
|
+
this.#bytes -= size;
|
|
143
|
+
if (entry.hits === 0)
|
|
144
|
+
this.#coldBytes -= size;
|
|
145
|
+
}
|
|
146
|
+
#age() {
|
|
147
|
+
this.#puts += 1;
|
|
148
|
+
if (this.#puts % HIT_AGING_INTERVAL !== 0)
|
|
149
|
+
return;
|
|
150
|
+
for (const [key, entry] of this.#entries) {
|
|
151
|
+
if (entry.hits === 0)
|
|
152
|
+
continue;
|
|
153
|
+
entry.hits = Math.floor(entry.hits / 2);
|
|
154
|
+
if (entry.hits === 0)
|
|
155
|
+
this.#coldBytes += entrySize(key, entry.encoded);
|
|
156
|
+
}
|
|
94
157
|
}
|
|
95
158
|
clear() {
|
|
96
159
|
this.#entries.clear();
|
|
160
|
+
this.#doorkeeper.clear();
|
|
97
161
|
this.#bytes = 0;
|
|
162
|
+
this.#coldBytes = 0;
|
|
98
163
|
}
|
|
99
164
|
statistics() {
|
|
100
165
|
return {
|
|
101
166
|
hits: this.#hits,
|
|
102
167
|
misses: this.#misses,
|
|
103
168
|
evictions: this.#evictions,
|
|
169
|
+
rejections: this.#rejections,
|
|
104
170
|
retainedBytes: this.#bytes,
|
|
105
171
|
retainedDocuments: this.#entries.size,
|
|
106
172
|
};
|
|
107
173
|
}
|
|
108
174
|
}
|
|
175
|
+
function entrySize(key, encoded) {
|
|
176
|
+
return encoded.length + key.length * 2 + 128;
|
|
177
|
+
}
|
|
109
178
|
// All domains call the same parser; cache keys include both bytes and source location.
|
|
110
179
|
// Authorization remains in workspace reads, before any document reaches this cache.
|
|
111
180
|
export const sourceDocuments = new SourceDocumentCache();
|
|
@@ -1308,6 +1308,13 @@ export class EventChainViewer {
|
|
|
1308
1308
|
input.signal?.throwIfAborted();
|
|
1309
1309
|
if (input.proposedSources !== undefined)
|
|
1310
1310
|
assertProposedSourceBounds(input.proposedSources);
|
|
1311
|
+
// Without references the comparison uses the previous cached revision; with none cached,
|
|
1312
|
+
// say so before scanning the workspace.
|
|
1313
|
+
if (input.before === undefined &&
|
|
1314
|
+
input.after === undefined &&
|
|
1315
|
+
input.proposedSources === undefined &&
|
|
1316
|
+
(this.#state.history.get(input.workspaceId)?.size ?? 0) === 0)
|
|
1317
|
+
throw new ServiceError('EVENT_COMPARISON_BASELINE_REQUIRED', 'Provide a cached revision, graph artifact, or proposed source overlay');
|
|
1311
1318
|
const refresh = input.refresh ??
|
|
1312
1319
|
(input.proposedSources !== undefined ||
|
|
1313
1320
|
input.before === undefined ||
|
|
@@ -456,6 +456,12 @@ function overlayToSvg(scene, variant) {
|
|
|
456
456
|
.join('');
|
|
457
457
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${scene.resolution.width}" height="${scene.resolution.height}" viewBox="0 0 ${scene.resolution.width} ${scene.resolution.height}"><defs>${toolText.definitions()}</defs>${overlays}</svg>`;
|
|
458
458
|
}
|
|
459
|
+
/** The preview backdrop; a transparent scenario leaves uncovered pixels fully transparent. */
|
|
460
|
+
function backdrop(scene, viewBox) {
|
|
461
|
+
return scene.scenario.background === 'transparent'
|
|
462
|
+
? ''
|
|
463
|
+
: `<rect ${rectAttributes(viewBox)} fill="#17202a"/>`;
|
|
464
|
+
}
|
|
459
465
|
export function sceneToSvg(scene, variant) {
|
|
460
466
|
const view = viewFor(scene, variant);
|
|
461
467
|
assertRenderDimensions(view.width, view.height, `GUI ${variant} SVG`);
|
|
@@ -470,7 +476,7 @@ export function sceneToSvg(scene, variant) {
|
|
|
470
476
|
.map((element) => renderOverlay(element, variant, toolText))
|
|
471
477
|
.join('');
|
|
472
478
|
const fidelity = escapeXml(canonicalJson(scene.fidelity));
|
|
473
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" width="${view.width}" height="${view.height}" viewBox="${finite(view.viewBox.x)} ${finite(view.viewBox.y)} ${finite(view.viewBox.width)} ${finite(view.viewBox.height)}"><metadata data-renderer="hoi4-agent-tools" data-mode="offline">${fidelity}</metadata><defs>${clipDefinitions.join('')}${sceneGlyphDefinitions(scene)}${toolText.definitions()}</defs
|
|
479
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${view.width}" height="${view.height}" viewBox="${finite(view.viewBox.x)} ${finite(view.viewBox.y)} ${finite(view.viewBox.width)} ${finite(view.viewBox.height)}"><metadata data-renderer="hoi4-agent-tools" data-mode="offline">${fidelity}</metadata><defs>${clipDefinitions.join('')}${sceneGlyphDefinitions(scene)}${toolText.definitions()}</defs>${backdrop(scene, view.viewBox)}${body}${overlays}</svg>`;
|
|
474
480
|
}
|
|
475
481
|
async function cooperativeParts(values, render, signal) {
|
|
476
482
|
const output = [];
|
|
@@ -494,7 +500,7 @@ async function sceneToSvgCooperative(scene, variant, signal) {
|
|
|
494
500
|
const body = (await cooperativeParts(scene.elements, (element, index) => renderBaseElement(element, element.clipRect === undefined ? undefined : `clip-${index}`, toolText), signal)) + renderTooltipPreview(scene, toolText);
|
|
495
501
|
const overlays = await cooperativeParts(scene.elements, (element) => renderOverlay(element, variant, toolText), signal);
|
|
496
502
|
const fidelity = escapeXml(canonicalJson(scene.fidelity));
|
|
497
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" width="${view.width}" height="${view.height}" viewBox="${finite(view.viewBox.x)} ${finite(view.viewBox.y)} ${finite(view.viewBox.width)} ${finite(view.viewBox.height)}"><metadata data-renderer="hoi4-agent-tools" data-mode="offline">${fidelity}</metadata><defs>${clipDefinitions}${sceneGlyphDefinitions(scene)}${toolText.definitions()}</defs
|
|
503
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${view.width}" height="${view.height}" viewBox="${finite(view.viewBox.x)} ${finite(view.viewBox.y)} ${finite(view.viewBox.width)} ${finite(view.viewBox.height)}"><metadata data-renderer="hoi4-agent-tools" data-mode="offline">${fidelity}</metadata><defs>${clipDefinitions}${sceneGlyphDefinitions(scene)}${toolText.definitions()}</defs>${backdrop(scene, view.viewBox)}${body}${overlays}</svg>`;
|
|
498
504
|
}
|
|
499
505
|
export function hierarchyToSvg(scene) {
|
|
500
506
|
const rows = scene.elements.toSorted((left, right) => left.depth - right.depth || left.zIndex - right.zIndex || compareCodeUnits(left.id, right.id));
|
|
@@ -459,6 +459,62 @@ function generatedRows(count, random, options) {
|
|
|
459
459
|
};
|
|
460
460
|
});
|
|
461
461
|
}
|
|
462
|
+
/** Sprite tokens read by scripted GUI `image` properties of the window. */
|
|
463
|
+
function scriptedImageTokens(scripted) {
|
|
464
|
+
const tokens = new Set();
|
|
465
|
+
for (const definition of scripted)
|
|
466
|
+
for (const property of definition.propertyDefinitions)
|
|
467
|
+
for (const [attribute, expression] of Object.entries(property.attributes)) {
|
|
468
|
+
const token = expressionToken(expression);
|
|
469
|
+
if (token !== undefined && attribute.toLocaleLowerCase('en-US') === 'image')
|
|
470
|
+
tokens.add(token);
|
|
471
|
+
}
|
|
472
|
+
return tokens;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Resolve the window's scripted localisation exactly under a supplied scenario: a text or
|
|
476
|
+
* sprite token whose `defined_text` branch the scenario's flags, variables and values decide
|
|
477
|
+
* takes that branch. A token the scenario cannot decide is left out, so it still renders as
|
|
478
|
+
* `[dynamic_loc]`, and caller-supplied values always win.
|
|
479
|
+
*/
|
|
480
|
+
export function resolveScenarioScriptedLocalisation(graph, windowName, base,
|
|
481
|
+
/** Built only when the window shows scripted localisation; building scans every helper. */
|
|
482
|
+
definitions = () => new ClausewitzEvaluationDefinitions()) {
|
|
483
|
+
if (graph.scriptedLocalisation.length === 0)
|
|
484
|
+
return base;
|
|
485
|
+
const scripted = relatedScriptedGuis(graph, windowName);
|
|
486
|
+
const elements = relevantElements(graph, windowName, scripted);
|
|
487
|
+
const textual = tokensIn(sourceTexts(graph, elements, base.language, scripted)).textual;
|
|
488
|
+
const names = new Set(graph.scriptedLocalisation.map(({ name }) => name));
|
|
489
|
+
const shown = (token) => names.has(token.slice(token.lastIndexOf('.') + 1));
|
|
490
|
+
const imageTokens = scriptedImageTokens(scripted);
|
|
491
|
+
if (![...textual, ...imageTokens].some(shown))
|
|
492
|
+
return base;
|
|
493
|
+
const evaluation = definitions();
|
|
494
|
+
const explicitValues = guiExplicitConditionValues(base);
|
|
495
|
+
const values = { ...base.values };
|
|
496
|
+
const templates = new Map();
|
|
497
|
+
const evaluate = (key, kind) => explicitValues[key] !== undefined
|
|
498
|
+
? undefined
|
|
499
|
+
: scriptedLocalisationValue(graph, key, base.language, kind, explicitValues, base, evaluation, []);
|
|
500
|
+
for (const key of textual) {
|
|
501
|
+
const template = evaluate(key, 'text');
|
|
502
|
+
if (template !== undefined)
|
|
503
|
+
templates.set(key, template);
|
|
504
|
+
}
|
|
505
|
+
for (const key of imageTokens) {
|
|
506
|
+
const sprite = evaluate(key, 'image');
|
|
507
|
+
if (sprite !== undefined)
|
|
508
|
+
values[key] = sprite;
|
|
509
|
+
}
|
|
510
|
+
// Branch texts can embed further tokens, including other resolved branches.
|
|
511
|
+
for (let pass = 0; pass < 3; pass += 1)
|
|
512
|
+
for (const [key, template] of templates)
|
|
513
|
+
values[key] = materializeDynamicText(template, { ...explicitValues, ...values });
|
|
514
|
+
return Object.keys(values).length === Object.keys(base.values).length
|
|
515
|
+
? base
|
|
516
|
+
: { ...base, values };
|
|
517
|
+
}
|
|
462
518
|
export function generateGuiPreviewScenarios(graph, windowName, base, options, definitions = new ClausewitzEvaluationDefinitions()) {
|
|
463
519
|
if (!options.enabled)
|
|
464
520
|
return [];
|
|
@@ -145,6 +145,8 @@ const GuiPreviewScenarioBodySchema = z
|
|
|
145
145
|
conditionResults: z.array(ConditionResultSchema).max(10_000).optional(),
|
|
146
146
|
variables: z.record(z.string(), z.number()).default({}),
|
|
147
147
|
flags: z.record(z.string(), z.boolean()).default({}),
|
|
148
|
+
closedFlags: z.boolean().optional(),
|
|
149
|
+
background: z.enum(['scene', 'transparent']).optional(),
|
|
148
150
|
lists: z.record(z.string(), z.array(objectSchema).max(10_000)).default({}),
|
|
149
151
|
localisation: z.record(z.string(), z.string()).default({}),
|
|
150
152
|
values: objectSchema.default({}),
|
|
@@ -21,7 +21,7 @@ import { projectGuiGraphForArtifact } from './inspection-artifact.js';
|
|
|
21
21
|
import { buildGuiScene } from './layout.js';
|
|
22
22
|
import { GUI_VALIDATION_MAX_DIAGNOSTICS } from './limits.js';
|
|
23
23
|
import { compareGuiImages, galleryDimensions, renderGallerySvg, renderGuiScene, } from './renderer.js';
|
|
24
|
-
import { generateGuiPreviewScenarios } from './scenario-generator.js';
|
|
24
|
+
import { generateGuiPreviewScenarios, resolveScenarioScriptedLocalisation, } from './scenario-generator.js';
|
|
25
25
|
import { parseGeneratedScenarioOptions, parsePreviewScenario } from './scenario.js';
|
|
26
26
|
import { buildGuiSourceGraph } from './source-graph.js';
|
|
27
27
|
import { assertGuiSourcePatchesSafe } from './source-patch.js';
|
|
@@ -917,6 +917,8 @@ export function guiArtifactProvenance(graph, kind, primaryScene, scenarioScenes
|
|
|
917
917
|
};
|
|
918
918
|
}
|
|
919
919
|
const sharedGuiGraphs = new WeakMap();
|
|
920
|
+
/** Graphs kept per engine: one for each scope a preview builds, across a few workspaces. */
|
|
921
|
+
const GUI_GRAPH_CACHE_SCOPES = 6;
|
|
920
922
|
export class ScriptedGuiStudio {
|
|
921
923
|
engine;
|
|
922
924
|
resolver;
|
|
@@ -953,12 +955,21 @@ export class ScriptedGuiStudio {
|
|
|
953
955
|
return cached;
|
|
954
956
|
}
|
|
955
957
|
const retainedFiles = [...files];
|
|
958
|
+
// The previous graph of this scope still indexes every unchanged file.
|
|
959
|
+
const previous = [...this.#graphCache].find(([existing]) => existing.startsWith(`${scope}:`))?.[1].index;
|
|
960
|
+
const index = this.engine.indexFiles(retainedFiles, previous);
|
|
956
961
|
const scanned = {
|
|
957
962
|
files: retainedFiles,
|
|
958
|
-
graph: buildGuiSourceGraph(retainedFiles,
|
|
963
|
+
graph: buildGuiSourceGraph(retainedFiles, index, additionalLocalisationKeys),
|
|
964
|
+
index,
|
|
959
965
|
};
|
|
966
|
+
// One graph per scope: a preview builds layout, game-layout and definition graphs, and a
|
|
967
|
+
// single shared slot made each repeat call rebuild all of them.
|
|
968
|
+
for (const existing of this.#graphCache.keys())
|
|
969
|
+
if (existing.startsWith(`${scope}:`))
|
|
970
|
+
this.#graphCache.delete(existing);
|
|
960
971
|
this.#graphCache.set(key, scanned);
|
|
961
|
-
while (this.#graphCache.size >
|
|
972
|
+
while (this.#graphCache.size > GUI_GRAPH_CACHE_SCOPES) {
|
|
962
973
|
const oldest = this.#graphCache.keys().next().value;
|
|
963
974
|
if (oldest === undefined)
|
|
964
975
|
break;
|
|
@@ -1004,7 +1015,7 @@ export class ScriptedGuiStudio {
|
|
|
1004
1015
|
const gameGraph = this.graphForFiles(gameLayouts.files, `game-layout:${workspaceId}`).graph;
|
|
1005
1016
|
const selectedPaths = new Set(selectedElements(gameGraph, windowName, [...requiredNames]).map(({ sourcePath }) => sourcePath));
|
|
1006
1017
|
layoutFiles = mergeScannedFiles(layoutFiles, gameLayouts.files.filter(({ displayPath }) => selectedPaths.has(displayPath)));
|
|
1007
|
-
layoutGraph = this.graphForFiles(layoutFiles, `layout:${workspaceId}`).graph;
|
|
1018
|
+
layoutGraph = this.graphForFiles(layoutFiles, `merged-layout:${workspaceId}`).graph;
|
|
1008
1019
|
}
|
|
1009
1020
|
if (!layoutGraph.elements.some(({ name }) => name === windowName))
|
|
1010
1021
|
assertTargetWindowAvailable(layoutGraph, windowName);
|
|
@@ -1062,16 +1073,20 @@ export class ScriptedGuiStudio {
|
|
|
1062
1073
|
? undefined
|
|
1063
1074
|
: parseGeneratedScenarioOptions(input.generatedScenarios);
|
|
1064
1075
|
const scanned = await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [placeholderScenario.language, ...explicitRelatedScenarios.map(({ language }) => language)], scenarioSpriteNames([placeholderScenario, ...explicitRelatedScenarios], generatedOptions?.textSamples), scenarioTemplateNames([placeholderScenario, ...explicitRelatedScenarios]), scenarioCountryFlagPatterns([placeholderScenario, ...explicitRelatedScenarios]), scenarioLocalisationKeys([placeholderScenario, ...explicitRelatedScenarios]), [placeholderScenario, ...explicitRelatedScenarios].some(({ tooltip }) => tooltip !== undefined));
|
|
1076
|
+
let builtDefinitions;
|
|
1077
|
+
const evaluationDefinitions = () => (builtDefinitions ??= ClausewitzEvaluationDefinitions.build(scanned));
|
|
1078
|
+
const resolveLocalisation = (target) => resolveScenarioScriptedLocalisation(scanned.graph, input.windowName, target, evaluationDefinitions);
|
|
1079
|
+
const suppliedScenario = resolveLocalisation(placeholderScenario);
|
|
1065
1080
|
const generatedScenarios = generatedOptions === undefined
|
|
1066
1081
|
? []
|
|
1067
|
-
: generateGuiPreviewScenarios(scanned.graph, input.windowName, placeholderScenario, generatedOptions,
|
|
1068
|
-
const scenario = generatedScenarios[0] ??
|
|
1082
|
+
: generateGuiPreviewScenarios(scanned.graph, input.windowName, placeholderScenario, generatedOptions, evaluationDefinitions());
|
|
1083
|
+
const scenario = generatedScenarios[0] ?? suppliedScenario;
|
|
1069
1084
|
const relatedScenarios = [
|
|
1070
1085
|
...(generatedOptions?.preservePlaceholder === true && generatedScenarios.length > 0
|
|
1071
|
-
? [
|
|
1086
|
+
? [suppliedScenario]
|
|
1072
1087
|
: []),
|
|
1073
1088
|
...generatedScenarios.slice(1),
|
|
1074
|
-
...explicitRelatedScenarios,
|
|
1089
|
+
...explicitRelatedScenarios.map(resolveLocalisation),
|
|
1075
1090
|
];
|
|
1076
1091
|
assertUniqueScenarioIds([scenario, ...relatedScenarios]);
|
|
1077
1092
|
const budget = new RenderBudget();
|
|
@@ -1157,16 +1172,20 @@ export class ScriptedGuiStudio {
|
|
|
1157
1172
|
...(requestedBaselineScenario === undefined ? [] : [requestedBaselineScenario]),
|
|
1158
1173
|
...explicitRelatedScenarios,
|
|
1159
1174
|
].some(({ tooltip }) => tooltip !== undefined));
|
|
1175
|
+
let builtDefinitions;
|
|
1176
|
+
const evaluationDefinitions = () => (builtDefinitions ??= ClausewitzEvaluationDefinitions.build(scanned));
|
|
1177
|
+
const resolveLocalisation = (target) => resolveScenarioScriptedLocalisation(scanned.graph, input.windowName, target, evaluationDefinitions);
|
|
1178
|
+
const suppliedScenario = resolveLocalisation(placeholderScenario);
|
|
1160
1179
|
const generatedScenarios = generatedOptions === undefined
|
|
1161
1180
|
? []
|
|
1162
|
-
: generateGuiPreviewScenarios(scanned.graph, input.windowName, placeholderScenario, generatedOptions,
|
|
1163
|
-
const scenario = generatedScenarios[0] ??
|
|
1181
|
+
: generateGuiPreviewScenarios(scanned.graph, input.windowName, placeholderScenario, generatedOptions, evaluationDefinitions());
|
|
1182
|
+
const scenario = generatedScenarios[0] ?? suppliedScenario;
|
|
1164
1183
|
const relatedScenarios = [
|
|
1165
1184
|
...(generatedOptions?.preservePlaceholder === true && generatedScenarios.length > 0
|
|
1166
|
-
? [
|
|
1185
|
+
? [suppliedScenario]
|
|
1167
1186
|
: []),
|
|
1168
1187
|
...generatedScenarios.slice(1),
|
|
1169
|
-
...explicitRelatedScenarios,
|
|
1188
|
+
...explicitRelatedScenarios.map(resolveLocalisation),
|
|
1170
1189
|
];
|
|
1171
1190
|
assertUniqueScenarioIds([scenario, ...relatedScenarios]);
|
|
1172
1191
|
const baselineScenario = requestedBaselineScenario ??
|
|
@@ -4,6 +4,9 @@ import { sha256Bytes } from '../core/canonical.js';
|
|
|
4
4
|
import { assertRenderDimensions } from '../core/render-budget.js';
|
|
5
5
|
import { GuiAssetCatalog } from './assets.js';
|
|
6
6
|
import { GUI_GRAPH_MAX_EDGES, GUI_GRAPH_MAX_NODES, GUI_SCENE_MAX_ELEMENTS, GUI_VALIDATION_MAX_ANCESTOR_HOPS, GUI_VALIDATION_MAX_DIAGNOSTICS, GUI_VALIDATION_MAX_PAIR_COMPARISONS, } from './limits.js';
|
|
7
|
+
/** Horizontal offsets from a parent's centre above rounding and up to a few pixels look accidental. */
|
|
8
|
+
const NEAR_CENTRE_TOLERANCE_PX = 0.5;
|
|
9
|
+
const NEAR_CENTRE_MAX_PX = 3;
|
|
7
10
|
const allStates = [
|
|
8
11
|
'normal',
|
|
9
12
|
'hover',
|
|
@@ -435,6 +438,19 @@ function validateOverlapAndGeometry(scene, diagnostics, work) {
|
|
|
435
438
|
const visible = scene.elements.filter((element) => element.visible && area(element.rect) > 0);
|
|
436
439
|
const byId = new Map(scene.elements.map((element) => [element.id, element]));
|
|
437
440
|
for (const element of scene.elements) {
|
|
441
|
+
// An element a few pixels from its parent's horizontal centre was almost always meant
|
|
442
|
+
// to be centred; larger offsets are deliberate placement.
|
|
443
|
+
const parent = element.parentId === undefined ? undefined : byId.get(element.parentId);
|
|
444
|
+
if (element.visible &&
|
|
445
|
+
parent?.visible === true &&
|
|
446
|
+
element.unclippedRect.width > 0 &&
|
|
447
|
+
element.unclippedRect.width < parent.unclippedRect.width - 2) {
|
|
448
|
+
const offset = element.unclippedRect.x +
|
|
449
|
+
element.unclippedRect.width / 2 -
|
|
450
|
+
(parent.unclippedRect.x + parent.unclippedRect.width / 2);
|
|
451
|
+
if (Math.abs(offset) > NEAR_CENTRE_TOLERANCE_PX && Math.abs(offset) <= NEAR_CENTRE_MAX_PX)
|
|
452
|
+
diagnostics.push(issue('GUI_NEAR_CENTRE_OFFSET', 'warning', 'layout', `${element.name} sits ${Math.abs(offset).toFixed(1)} px ${offset > 0 ? 'right' : 'left'} of the centre of ${parent.name}; centre it exactly or offset it clearly.`, element, { parent: parent.id, offset }));
|
|
453
|
+
}
|
|
438
454
|
if (element.unclippedRect.width <= 0 || element.unclippedRect.height <= 0) {
|
|
439
455
|
diagnostics.push(issue('GUI_INVALID_SIZE', 'warning', 'layout', `${element.name} has a non-positive ${element.unclippedRect.width}x${element.unclippedRect.height} size.`, element));
|
|
440
456
|
}
|
|
@@ -477,6 +493,12 @@ function validateOverlapAndGeometry(scene, diagnostics, work) {
|
|
|
477
493
|
if (left.clickable && right.clickable && ratio > 0.02) {
|
|
478
494
|
diagnostics.push(issue('GUI_CONFLICTING_CLICK_REGIONS', 'error', 'design', `${left.name} and ${right.name} have overlapping click regions (${(ratio * 100).toFixed(1)}%).`, right, { left: left.id, right: right.id, ratio }));
|
|
479
495
|
}
|
|
496
|
+
// Tab pages and alternative panels overlap by design; the game shows one at a time.
|
|
497
|
+
// When the scenario cannot decide which, the preview draws them stacked.
|
|
498
|
+
if (left.depth === right.depth &&
|
|
499
|
+
ratio > 0.2 &&
|
|
500
|
+
(left.visibilityStatus === 'unresolved' || right.visibilityStatus === 'unresolved'))
|
|
501
|
+
diagnostics.push(issue('GUI_UNRESOLVED_VISIBILITY_OVERLAP', 'warning', 'layout', `${left.name} and ${right.name} overlap and the scenario does not decide which is visible, so the preview draws both. Declare the flags or variables their visible triggers read, with closedFlags, to render one of them.`, right, { left: left.id, right: right.id, ratio }));
|
|
480
502
|
if (left.depth === right.depth && ratio > 0.2) {
|
|
481
503
|
diagnostics.push(issue('GUI_VISIBLE_OVERLAP', 'warning', 'layout', `${left.name} and ${right.name} overlap by ${(ratio * 100).toFixed(1)}% of the smaller element.`, right, { left: left.id, right: right.id, ratio }));
|
|
482
504
|
}
|
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
import { ReadResourceResultSchema } from '@modelcontextprotocol/core';
|
|
2
2
|
import { ProtocolError, ProtocolErrorCode, ResourceNotFoundError, } from '@modelcontextprotocol/server';
|
|
3
3
|
import { artifactResourceDefinition, readArtifactResource } from '../../core/artifact-resource.js';
|
|
4
|
+
import { readServerStatus, serverStatusResource } from '../../core/server-status-resource.js';
|
|
4
5
|
import { ServiceError } from '../../core/result.js';
|
|
5
6
|
/** Modern wire projection of the same non-enumerating, authorization-bound resource reader. */
|
|
6
7
|
export function registerModernResources(server, engine, context) {
|
|
7
|
-
|
|
8
|
+
// Process details are offered to the local client only, never to authenticated remote ones.
|
|
9
|
+
const local = context.principal === undefined;
|
|
10
|
+
server.setRequestHandler('resources/list', () => ({
|
|
11
|
+
resources: local ? [serverStatusResource] : [],
|
|
12
|
+
}));
|
|
8
13
|
server.setRequestHandler('resources/templates/list', () => ({
|
|
9
14
|
resourceTemplates: [artifactResourceDefinition],
|
|
10
15
|
}));
|
|
11
16
|
server.setRequestHandler('resources/read', async (request, extra) => {
|
|
17
|
+
if (local && request.params.uri === serverStatusResource.uri)
|
|
18
|
+
return ReadResourceResultSchema.parse({
|
|
19
|
+
...(await readServerStatus(engine)),
|
|
20
|
+
resultType: 'complete',
|
|
21
|
+
ttlMs: 0,
|
|
22
|
+
cacheScope: 'private',
|
|
23
|
+
});
|
|
12
24
|
try {
|
|
13
25
|
return ReadResourceResultSchema.parse({
|
|
14
26
|
...(await readArtifactResource(engine, request.params.uri, context, extra.mcpReq.signal)),
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { artifactResourceDefinition, readArtifactResource } from '../../core/artifact-resource.js';
|
|
3
|
+
import { readServerStatus, serverStatusResource } from '../../core/server-status-resource.js';
|
|
3
4
|
/** SDK-v1 registration only; ownership and byte-range behavior live in the shared core. */
|
|
4
5
|
export function registerMcpResources(server, engine, context) {
|
|
5
6
|
const { name, uriTemplate, ...metadata } = artifactResourceDefinition;
|
|
6
7
|
server.registerResource(name, new ResourceTemplate(uriTemplate, { list: undefined }), metadata, (uri, _variables, extra) => readArtifactResource(engine, uri.href, context, extra.signal));
|
|
8
|
+
// Process details are offered to the local client only, never to authenticated remote ones.
|
|
9
|
+
if (context.principal !== undefined)
|
|
10
|
+
return;
|
|
11
|
+
const { name: statusName, uri: statusUri, ...statusMetadata } = serverStatusResource;
|
|
12
|
+
server.registerResource(statusName, statusUri, statusMetadata, () => readServerStatus(engine));
|
|
7
13
|
}
|
|
@@ -44,7 +44,7 @@ export const referenceTools = [
|
|
|
44
44
|
{
|
|
45
45
|
name: 'hoi4.script_validate',
|
|
46
46
|
title: 'Check HOI4 script commands and scopes',
|
|
47
|
-
description: 'Check effect or trigger snippet syntax, native command kinds and
|
|
47
|
+
description: 'Check an effect or trigger snippet (source, kind, scope) or a whole mod file (path) for syntax, native command kinds and scopes against cited installed documentation. Unknown helpers and native argument blocks remain explicit.',
|
|
48
48
|
inputSchema: scriptValidateRequestSchema,
|
|
49
49
|
outputSchema: strictOperationResultSchema(scriptValidateDataSchema),
|
|
50
50
|
annotations: readOnly,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { registerLegacyTaskTools } from '../server/legacy-task-tools.js';
|
|
2
2
|
import { z } from 'zod/v4';
|
|
3
3
|
import { helperExpansionRequestSchema, helperExpansionSummarySchema, } from '../../schemas/helper-expansion.js';
|
|
4
|
-
import { technologyAnalysisModeSchema, technologyCompareRequestSchema, technologyGraphReferenceSchema, technologyImpactSchema, technologyInspectRequestSchema, technologyProposedSourceSchema, technologyRenderRequestSchema, technologyRenderViewSchema, validateTechnologyCompareRequest, validateTechnologyInspectRequest, } from '../../schemas/technology.js';
|
|
4
|
+
import { technologyAnalysisModeSchema, technologyCompareRequestSchema, technologyGraphReferenceSchema, technologyImpactSchema, technologyInspectRequestSchema, technologyProposedSourceSchema, technologyRenderRequestSchema, validateTechnologyRenderRequest, technologyRenderViewSchema, validateTechnologyCompareRequest, validateTechnologyInspectRequest, } from '../../schemas/technology.js';
|
|
5
5
|
import { compactValidatedInputSchema } from '../server/context-schemas.js';
|
|
6
6
|
import { nonNegativeIntegerSchema, sha256Schema } from '../server/output-schemas.js';
|
|
7
7
|
import { strictOperationResultSchema } from '../server/result.js';
|
|
@@ -13,7 +13,10 @@ const inspectInputSchema = z
|
|
|
13
13
|
})
|
|
14
14
|
.strict()
|
|
15
15
|
.superRefine(validateTechnologyInspectRequest);
|
|
16
|
-
const renderInputSchema = z
|
|
16
|
+
const renderInputSchema = z
|
|
17
|
+
.object({ ...technologyRenderRequestSchema.shape })
|
|
18
|
+
.strict()
|
|
19
|
+
.superRefine(validateTechnologyRenderRequest);
|
|
17
20
|
const compareInputSchema = z
|
|
18
21
|
.object({
|
|
19
22
|
...technologyCompareRequestSchema.shape,
|
|
@@ -80,12 +80,21 @@ export async function serveNegotiatedStdio(engine, options) {
|
|
|
80
80
|
reportedErrors.add(error);
|
|
81
81
|
options.onerror(error);
|
|
82
82
|
};
|
|
83
|
+
const observer = options.observer;
|
|
84
|
+
if (observer !== undefined) {
|
|
85
|
+
const send = physical.send.bind(physical);
|
|
86
|
+
physical.send = (message) => {
|
|
87
|
+
observer.sent(message);
|
|
88
|
+
return send(message);
|
|
89
|
+
};
|
|
90
|
+
}
|
|
83
91
|
physical.onclose = () => pinned?.onclose();
|
|
84
92
|
physical.onerror = (error) => {
|
|
85
93
|
reportError(error);
|
|
86
94
|
pinned?.onerror(error);
|
|
87
95
|
};
|
|
88
96
|
physical.onmessage = (message) => {
|
|
97
|
+
observer?.received(message);
|
|
89
98
|
if (pinned === undefined) {
|
|
90
99
|
pinned = new PinnedStdioTransport(physical, reportError);
|
|
91
100
|
try {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ends a stdio server that no client can reach. A client normally closes stdin, which the
|
|
3
|
+
* transport turns into a clean exit. Two cases leave stdin open: the launching process
|
|
4
|
+
* (for example a `cmd.exe` shim that a client terminated) dies while the client keeps the
|
|
5
|
+
* pipe, and a client keeps an idle connection it will never use again. The first is detected
|
|
6
|
+
* by polling the parent process; the second by an optional idle limit.
|
|
7
|
+
*/
|
|
8
|
+
import { processAlive } from '../../core/server-registry.js';
|
|
9
|
+
const PARENT_POLL_MS = 5_000;
|
|
10
|
+
export { processAlive };
|
|
11
|
+
export class StdioLifetime {
|
|
12
|
+
#options;
|
|
13
|
+
#open = new Set();
|
|
14
|
+
#lastActivity;
|
|
15
|
+
#timer;
|
|
16
|
+
#stopped = false;
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.#options = {
|
|
19
|
+
parentPid: options.parentPid ?? process.ppid,
|
|
20
|
+
isAlive: options.isAlive ?? processAlive,
|
|
21
|
+
now: options.now ?? Date.now,
|
|
22
|
+
pollMs: options.pollMs ?? PARENT_POLL_MS,
|
|
23
|
+
idleExitMinutes: options.idleExitMinutes,
|
|
24
|
+
onExit: options.onExit,
|
|
25
|
+
};
|
|
26
|
+
this.#lastActivity = this.#options.now();
|
|
27
|
+
}
|
|
28
|
+
start() {
|
|
29
|
+
this.#timer = setInterval(() => this.check(), this.#options.pollMs);
|
|
30
|
+
// The poll never keeps an otherwise finished server alive.
|
|
31
|
+
this.#timer.unref();
|
|
32
|
+
}
|
|
33
|
+
stop() {
|
|
34
|
+
this.#stopped = true;
|
|
35
|
+
if (this.#timer !== undefined)
|
|
36
|
+
clearInterval(this.#timer);
|
|
37
|
+
}
|
|
38
|
+
/** A client message: a request opens until its response is sent. */
|
|
39
|
+
received(message) {
|
|
40
|
+
this.#lastActivity = this.#options.now();
|
|
41
|
+
const id = requestId(message, 'method');
|
|
42
|
+
if (id !== undefined)
|
|
43
|
+
this.#open.add(id);
|
|
44
|
+
}
|
|
45
|
+
/** A server message: a response closes its request. */
|
|
46
|
+
sent(message) {
|
|
47
|
+
this.#lastActivity = this.#options.now();
|
|
48
|
+
const id = requestId(message, 'result') ?? requestId(message, 'error');
|
|
49
|
+
if (id !== undefined)
|
|
50
|
+
this.#open.delete(id);
|
|
51
|
+
}
|
|
52
|
+
/** When the client last sent or received a message, in epoch milliseconds. */
|
|
53
|
+
lastActivity() {
|
|
54
|
+
return this.#lastActivity;
|
|
55
|
+
}
|
|
56
|
+
check() {
|
|
57
|
+
if (this.#stopped)
|
|
58
|
+
return;
|
|
59
|
+
const { parentPid, isAlive, idleExitMinutes, now, onExit } = this.#options;
|
|
60
|
+
// A parent pid of 0 or 1 means the server was already reparented at start.
|
|
61
|
+
if (parentPid > 1 && !isAlive(parentPid)) {
|
|
62
|
+
this.stop();
|
|
63
|
+
onExit('parent_exited');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (idleExitMinutes > 0 &&
|
|
67
|
+
this.#open.size === 0 &&
|
|
68
|
+
now() - this.#lastActivity > idleExitMinutes * 60_000) {
|
|
69
|
+
this.stop();
|
|
70
|
+
onExit('idle');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function requestId(message, field) {
|
|
75
|
+
if (typeof message !== 'object' || message === null || !(field in message))
|
|
76
|
+
return undefined;
|
|
77
|
+
const id = message.id;
|
|
78
|
+
return typeof id === 'string' || typeof id === 'number' ? id : undefined;
|
|
79
|
+
}
|