comfyui-mcp 0.51.12 → 0.51.13

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.
@@ -35,6 +35,40 @@ import { listSessions, loadTranscript } from "./history.js";
35
35
  import { uploadImageHttp, resetClient } from "../comfyui/client.js";
36
36
  import { setConnectedPanelOrigins } from "../comfyui/fetch.js";
37
37
  import { logger } from "../utils/logger.js";
38
+ import { assembleVocabularyHash, describeVocabularySkew } from "../tools/vocabulary.js";
39
+ import { buildPanelToolDefs } from "./panel-tools.js";
40
+ /** The panel vocabulary hashes whose MISMATCH has already been reported (#236).
41
+ *
42
+ * Keyed by the HASH ALONE, not by tab. Codex round 2 found that a per-tab key lets a
43
+ * stale entry suppress a real mismatch: `wf:` ids are reused, so a tab that closes
44
+ * after warning leaves an entry that silences the NEXT tab opening the same workflow.
45
+ *
46
+ * Keying by hash is not a patch for that, it is the correct granularity. The fact
47
+ * being reported — "the vocabulary this panel build vendored disagrees with this
48
+ * server's" — is a property of the panel BUILD, not of a tab or a workflow. Saying it
49
+ * once per distinct disagreeing vocabulary is exactly the news there is; saying it
50
+ * once per tab was always repeating one fact under different labels.
51
+ *
52
+ * It also bounds itself: a process sees one or two panel builds, so the set holds one
53
+ * or two entries where the per-tab map grew with every id ever minted. */
54
+ const loggedVocabularySkew = new Set();
55
+ /** A cap anyway (codex round 1, P2). Nothing legitimate mints hashes — a panel
56
+ * advertises one per build — but this reads a value off the wire, and a broken or
57
+ * hand-modified panel that sent a fresh hash every hello would otherwise grow the set
58
+ * without limit. Oldest-first eviction, and the direction stays safe: an evicted entry
59
+ * can only cause a mismatch to be reported a SECOND time, never suppress one, because
60
+ * entries only ever suppress. */
61
+ const MAX_LOGGED_VOCABULARY_SKEW = 64;
62
+ /** This server's vocabulary hash, computed once.
63
+ *
64
+ * Memoised because it cannot change without a restart (the tool surface is fixed at
65
+ * build time) and buildPanelToolDefs() walks every panel tool definition — work that
66
+ * has no business running on every hello, which is a hot path during a reconnect loop. */
67
+ let cachedServerVocabularyHash;
68
+ function serverVocabularyHash() {
69
+ cachedServerVocabularyHash ??= assembleVocabularyHash(buildPanelToolDefs().map((d) => d.name));
70
+ return cachedServerVocabularyHash;
71
+ }
38
72
  import { PanelAgentManager, fetchSupportedModels, fetchSupportedCommands, isEffort, } from "./panel-agent.js";
39
73
  import { promptText } from "./error-text.js";
40
74
  import { callToolAdmission } from "./call-tool-admission.js";
@@ -3149,6 +3183,46 @@ export async function runPanelOrchestrator() {
3149
3183
  latestPanelVersion = helloPanelVer;
3150
3184
  void refreshEnvCapabilities();
3151
3185
  }
3186
+ // #236 — THE VOCABULARY HANDSHAKE, at the handshake.
3187
+ //
3188
+ // `describeVocabularySkew` and the hash it compares have existed, fully
3189
+ // unit-tested, since the #683 follow-up, and NOTHING called them: the bridge
3190
+ // stored the panel's advertised hash on every hello under a comment promising
3191
+ // this exact check, and no code read the field. A mechanism can be completely
3192
+ // unreachable and still pass every test it has — the tests proved the function
3193
+ // worked, never that it ran — so the call site below is asserted directly, by
3194
+ // source, in vocabulary-handshake.test.ts.
3195
+ //
3196
+ // The comparison is done HERE rather than in the bridge because the server's own
3197
+ // hash needs buildPanelToolDefs(), and panel-tools.ts imports ui-bridge.ts — the
3198
+ // bridge cannot reach it without a cycle. The orchestrator already imports both.
3199
+ const helloVocabHash = event.vocabulary_hash;
3200
+ {
3201
+ const advertised = typeof helloVocabHash === "string" && helloVocabHash ? helloVocabHash : undefined;
3202
+ const skew = describeVocabularySkew(serverVocabularyHash(), advertised, typeof helloPanelVer === "string" ? helloPanelVer : undefined);
3203
+ // Reported once per DISTINCT disagreeing vocabulary, for the whole process.
3204
+ // A reconnect ping-pong repeats the same hello every few seconds and the same
3205
+ // skew is not new news; a panel that UPDATES to a different (still disagreeing)
3206
+ // vocabulary is, and its new hash reports again.
3207
+ //
3208
+ // There is deliberately NO "clear on match" here. Under the per-tab key a
3209
+ // stale entry had to be cleared so a tab that came back into agreement and
3210
+ // later regressed would report again; keyed by hash there is nothing stale to
3211
+ // clear — a regression re-advertises the same disagreeing hash, and if it was
3212
+ // already reported, that is genuinely the same news the user already has.
3213
+ if (skew.status === "mismatch" && !loggedVocabularySkew.has(advertised)) {
3214
+ // Insertion-ordered, so the first entry is the oldest. Evicted BEFORE the
3215
+ // add so the cap is a real bound rather than a bound-plus-one.
3216
+ while (loggedVocabularySkew.size >= MAX_LOGGED_VOCABULARY_SKEW) {
3217
+ const oldest = loggedVocabularySkew.values().next().value;
3218
+ if (oldest === undefined)
3219
+ break;
3220
+ loggedVocabularySkew.delete(oldest);
3221
+ }
3222
+ loggedVocabularySkew.add(advertised);
3223
+ logger.warn(`[panel-orchestrator] ${skew.message}`);
3224
+ }
3225
+ }
3152
3226
  // #706 — an npm-orchestrator update can require a newer Registry panel.
3153
3227
  // The panel-sync service owns the ENTIRE safety decision: it re-reads the
3154
3228
  // local install under the panel-op lock, refuses pins/dev installs/shadows