comfyui-mcp 0.51.49 → 0.51.51
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/dist/orchestrator/download-done-guard.js +94 -0
- package/dist/orchestrator/download-done-guard.js.map +1 -0
- package/dist/orchestrator/index.js +45 -1
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/orchestrator/panel-agent.js +20 -2
- package/dist/orchestrator/panel-agent.js.map +1 -1
- package/dist/services/manifest.js +135 -3
- package/dist/services/manifest.js.map +1 -1
- package/dist/services/ui-bridge.js +111 -5
- package/dist/services/ui-bridge.js.map +1 -1
- package/dist/tools/manifest.js +5 -1
- package/dist/tools/manifest.js.map +1 -1
- package/package.json +4 -3
- package/scripts/check-docs-locale.mjs +231 -0
- package/scripts/gen-tool-docs.ts +26 -4
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #1574 — when the completion event and `download_model action:"status"` disagree, say so.
|
|
3
|
+
*
|
|
4
|
+
* The tray raised `transfer completed` for an 11.46GB download while
|
|
5
|
+
* `download_model action:"status"` reported it still streaming, `list_local_models` showed the
|
|
6
|
+
* file absent, and the category count only rose minutes later. The file landed AFTER the
|
|
7
|
+
* event.
|
|
8
|
+
*
|
|
9
|
+
* The completion event is built from a PROGRESS ROW read off disk each tick. `status` answers
|
|
10
|
+
* from the JOB RECORD. Two stores, and the event consults only one — the same split #1545
|
|
11
|
+
* documented from the other side.
|
|
12
|
+
*
|
|
13
|
+
* ## Why this ANNOTATES and never suppresses
|
|
14
|
+
*
|
|
15
|
+
* The first version of this dropped the contradicted event. Review killed it, correctly: a
|
|
16
|
+
* terminal record may legitimately still read `downloading` until the ~15s persistence
|
|
17
|
+
* heartbeat retries (see `persistDownloadJob`'s return value, #1545). The debounce bucket is
|
|
18
|
+
* deleted before filtering and never requeued, so suppressing on a lagging record would
|
|
19
|
+
* PERMANENTLY lose the completion notification for a download that genuinely finished.
|
|
20
|
+
*
|
|
21
|
+
* That trades a confusing message for a missing one, which is worse: the user is waiting on
|
|
22
|
+
* that event. So the event always fires, and a disagreement is disclosed on it. The harm in
|
|
23
|
+
* the report is a CONFIDENT false completion — "the natural next action is to use the file" —
|
|
24
|
+
* and a hedge is precisely what removes that.
|
|
25
|
+
*
|
|
26
|
+
* ## Identity
|
|
27
|
+
*
|
|
28
|
+
* The two stores do not share an id. A progress row's `id` is the progress/tray identity;
|
|
29
|
+
* the job's `id` is its public status handle (`6226e26ba97f8527` in the report, against tray
|
|
30
|
+
* `93015fbfa0fa9933`). The row is matched against the job's `progressId ?? trayId`, which is
|
|
31
|
+
* what writes those rows. An earlier version compared row id to job id and was therefore
|
|
32
|
+
* INERT — it never matched anything, and its unit tests missed that because they fed
|
|
33
|
+
* synthetic rows carrying whatever id the assertion wanted.
|
|
34
|
+
*/
|
|
35
|
+
/** The identity a PROGRESS ROW is written under. */
|
|
36
|
+
function progressIdentityOf(job) {
|
|
37
|
+
const progress = typeof job.progressId === "string" ? job.progressId : null;
|
|
38
|
+
const tray = typeof job.trayId === "string" ? job.trayId : null;
|
|
39
|
+
return progress ?? tray;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Does the job record disagree with announcing this row as completed?
|
|
43
|
+
*
|
|
44
|
+
* True ONLY on a positive contradiction: a record exists for this exact progress identity and
|
|
45
|
+
* still says `downloading`. A missing record is not evidence — the record store resets on an
|
|
46
|
+
* orchestrator respawn, which is exactly the reported session.
|
|
47
|
+
*/
|
|
48
|
+
export function completionDisagreesWithRecord(row, jobs) {
|
|
49
|
+
if (!row || typeof row !== "object")
|
|
50
|
+
return false;
|
|
51
|
+
// Only COMPLETIONS. A failure event carries its own hedged wording (#1150) and must be
|
|
52
|
+
// left entirely alone.
|
|
53
|
+
if (row.status !== "done")
|
|
54
|
+
return false;
|
|
55
|
+
const id = typeof row.id === "string" ? row.id : null;
|
|
56
|
+
if (!id)
|
|
57
|
+
return false;
|
|
58
|
+
if (!Array.isArray(jobs))
|
|
59
|
+
return false;
|
|
60
|
+
// (id, target) — the SAME key the supersession logic uses, and for the same reason: a
|
|
61
|
+
// concurrent LOCAL and POD transfer of one URL shares an id but is two transfers with two
|
|
62
|
+
// outcomes. Matching on id alone could annotate the wrong completion, or miss a real
|
|
63
|
+
// disagreement by finding the other one first (review).
|
|
64
|
+
//
|
|
65
|
+
// A target is compared only when BOTH sides carry one. Rows and records that predate the
|
|
66
|
+
// field, or a route that never sets it, must not silently stop matching — that would make
|
|
67
|
+
// the check inert again, which is exactly how the first version shipped.
|
|
68
|
+
const target = typeof row.target === "string" ? row.target : null;
|
|
69
|
+
const sameId = jobs.filter((j) => j && typeof j === "object" && progressIdentityOf(j) === id);
|
|
70
|
+
if (!sameId.length)
|
|
71
|
+
return false;
|
|
72
|
+
// PREFER THE EXACT (id, target) MATCH (review, round 3). Taking the first id match in
|
|
73
|
+
// array order let a TARGETLESS record shadow the exact one: a targetless "downloading"
|
|
74
|
+
// sitting before an exact-target "done" reported a disagreement that does not exist, and
|
|
75
|
+
// would have hedged a completion that was perfectly fine.
|
|
76
|
+
//
|
|
77
|
+
// The targetless record still stands in when nothing matches on target — that is what
|
|
78
|
+
// keeps rows and records predating the field from silently going unmatched, which would
|
|
79
|
+
// make the whole check inert again.
|
|
80
|
+
const record = (target ? sameId.find((j) => j.target === target) : undefined) ??
|
|
81
|
+
sameId.find((j) => typeof j.target !== "string") ??
|
|
82
|
+
(target ? undefined : sameId[0]);
|
|
83
|
+
if (!record)
|
|
84
|
+
return false;
|
|
85
|
+
return record.status === "downloading";
|
|
86
|
+
}
|
|
87
|
+
/** The disclosure appended to a completion the record disagrees with. Deliberately states
|
|
88
|
+
* BOTH readings and what to do, rather than picking a winner this cannot establish. */
|
|
89
|
+
export const COMPLETION_DISAGREEMENT_NOTE = "CAVEAT: `download_model action:\"status\"` still reports this transfer as downloading. " +
|
|
90
|
+
"The two are read from different stores and the record can lag a real completion by a few " +
|
|
91
|
+
"seconds, so this may simply be that — but a completion event has also been observed to " +
|
|
92
|
+
"arrive minutes before the file existed (#1574). Check `download_model action:\"status\"` " +
|
|
93
|
+
"and that the file is present before loading it.";
|
|
94
|
+
//# sourceMappingURL=download-done-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"download-done-guard.js","sourceRoot":"","sources":["../../src/orchestrator/download-done-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAmBH,oDAAoD;AACpD,SAAS,kBAAkB,CAAC,GAAY;IACtC,MAAM,QAAQ,GAAG,OAAO,GAAG,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC;IAC5E,MAAM,IAAI,GAAG,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;IAChE,OAAO,QAAQ,IAAI,IAAI,CAAC;AAC1B,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,6BAA6B,CAAC,GAAY,EAAE,IAAwB;IAClF,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAClD,uFAAuF;IACvF,uBAAuB;IACvB,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM;QAAE,OAAO,KAAK,CAAC;IACxC,MAAM,EAAE,GAAG,OAAO,GAAG,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IACtD,IAAI,CAAC,EAAE;QAAE,OAAO,KAAK,CAAC;IACtB,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IACvC,sFAAsF;IACtF,0FAA0F;IAC1F,qFAAqF;IACrF,wDAAwD;IACxD,EAAE;IACF,yFAAyF;IACzF,0FAA0F;IAC1F,yEAAyE;IACzE,MAAM,MAAM,GAAG,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;IAClE,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,kBAAkB,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;IAC9F,IAAI,CAAC,MAAM,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IACjC,sFAAsF;IACtF,uFAAuF;IACvF,yFAAyF;IACzF,0DAA0D;IAC1D,EAAE;IACF,sFAAsF;IACtF,wFAAwF;IACxF,oCAAoC;IACpC,MAAM,MAAM,GACV,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAC9D,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,MAAM,KAAK,QAAQ,CAAC;QAChD,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IACnC,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAC1B,OAAO,MAAM,CAAC,MAAM,KAAK,aAAa,CAAC;AACzC,CAAC;AAED;wFACwF;AACxF,MAAM,CAAC,MAAM,4BAA4B,GACvC,yFAAyF;IACzF,2FAA2F;IAC3F,yFAAyF;IACzF,2FAA2F;IAC3F,iDAAiD,CAAC"}
|
|
@@ -36,6 +36,8 @@ import { uploadImageHttp, resetClient } from "../comfyui/client.js";
|
|
|
36
36
|
import { setConnectedPanelOrigins } from "../comfyui/fetch.js";
|
|
37
37
|
import { publishConnectedPanelOrigins } from "../services/panel-origin-channel.js";
|
|
38
38
|
import { logger } from "../utils/logger.js";
|
|
39
|
+
import { listDownloadJobs } from "../services/download-jobs.js";
|
|
40
|
+
import { completionDisagreesWithRecord } from "./download-done-guard.js";
|
|
39
41
|
import { assembleVocabularyHash, describeVocabularySkew } from "../tools/vocabulary.js";
|
|
40
42
|
import { buildPanelToolDefs } from "./panel-tools.js";
|
|
41
43
|
/** The panel vocabulary hashes whose MISMATCH has already been reported (#236).
|
|
@@ -5225,13 +5227,23 @@ export async function runPanelOrchestrator() {
|
|
|
5225
5227
|
const key = resolveDownloadAgentKey(row);
|
|
5226
5228
|
if (key && manager.hasLiveAgent(key)) {
|
|
5227
5229
|
const bucket = downloadDonePending.get(key) ??
|
|
5228
|
-
{
|
|
5230
|
+
{
|
|
5231
|
+
downloads: new Map(),
|
|
5232
|
+
flushAt: 0,
|
|
5233
|
+
};
|
|
5229
5234
|
// Identify each pending download by its (id, target) supersession key — NOT
|
|
5230
5235
|
// the id alone: a concurrent LOCAL + POD transfer of the same URL shares an id
|
|
5231
5236
|
// but must produce TWO #547 outcomes, and the same key lets a newer attempt
|
|
5232
5237
|
// evict this entry above. Fall back to the file path when the row has no id.
|
|
5233
5238
|
const supKey = downloadAttemptKey(row) ?? ` ${full}`;
|
|
5234
5239
|
bucket.downloads.set(supKey, {
|
|
5240
|
+
// #1574 — CARRY THE ROW ID. Without it the record cross-check at the flush has
|
|
5241
|
+
// nothing to match a job against, silently agrees with everything, and the
|
|
5242
|
+
// whole disclosure is a no-op. That is exactly how the first version shipped.
|
|
5243
|
+
id: typeof row.id === "string" ? row.id : undefined,
|
|
5244
|
+
// (id, target) is the row identity — id alone collides for a concurrent
|
|
5245
|
+
// LOCAL + POD transfer of the same URL (review).
|
|
5246
|
+
target: typeof row.target === "string" ? row.target : undefined,
|
|
5235
5247
|
name: String(row.name ?? row.id ?? "model"),
|
|
5236
5248
|
status: String(status),
|
|
5237
5249
|
attempt: typeof row.attempt === "number" ? row.attempt : undefined,
|
|
@@ -5288,6 +5300,38 @@ export async function runPanelOrchestrator() {
|
|
|
5288
5300
|
// filename, so the (id, target) eviction above cannot see it. The live rows
|
|
5289
5301
|
// are already in hand this tick; markSupersededByLive asks them by name.
|
|
5290
5302
|
markSupersededByLive(settled, downloads);
|
|
5303
|
+
// #1574 — DROP a completion this orchestrator's own status tool would contradict.
|
|
5304
|
+
//
|
|
5305
|
+
// The event is built from the progress ROW; `download_model action:"status"` answers
|
|
5306
|
+
// from the job RECORD. A reporter got "transfer completed" for an 11.46GB file while
|
|
5307
|
+
// status said it was still streaming and the file was not on disk — it landed minutes
|
|
5308
|
+
// later. Whatever wrote that row, the record is in hand right here.
|
|
5309
|
+
//
|
|
5310
|
+
// Only a POSITIVE contradiction drops it (the record exists, same id, still
|
|
5311
|
+
// "downloading"). An absent record means nothing: the record store resets on a
|
|
5312
|
+
// respawn, which is the reported session, and treating absence as in-flight would
|
|
5313
|
+
// silence every completion after any respawn.
|
|
5314
|
+
const records = (() => {
|
|
5315
|
+
try {
|
|
5316
|
+
return listDownloadJobs();
|
|
5317
|
+
}
|
|
5318
|
+
catch {
|
|
5319
|
+
// Never let the guard break the event path — a completion we cannot check is
|
|
5320
|
+
// still a completion worth delivering.
|
|
5321
|
+
return [];
|
|
5322
|
+
}
|
|
5323
|
+
})();
|
|
5324
|
+
// ANNOTATE, never suppress (review). A terminal record can legitimately still read
|
|
5325
|
+
// "downloading" until the ~15s persistence heartbeat retries (#1545), and this bucket
|
|
5326
|
+
// is deleted before this point and never requeued — so dropping the event here would
|
|
5327
|
+
// permanently lose the completion for a download that genuinely finished. That trades
|
|
5328
|
+
// a confusing message for a missing one, which is worse: the user is waiting on it.
|
|
5329
|
+
const disagreeing = settled.filter((d) => completionDisagreesWithRecord(d, records));
|
|
5330
|
+
for (const d of disagreeing)
|
|
5331
|
+
d.recordDisagrees = true;
|
|
5332
|
+
if (disagreeing.length) {
|
|
5333
|
+
logger.warn("[panel-orchestrator] a download completion disagrees with the job record; disclosing rather than suppressing (#1574)", { ids: disagreeing.map((d) => String(d.id ?? "")) });
|
|
5334
|
+
}
|
|
5291
5335
|
// #884 — a download has no originating TAB (its row names the owning
|
|
5292
5336
|
// conversation), so its turn INHERITS the conversation's LAST
|
|
5293
5337
|
// ESTABLISHED origin — never the active tab (confirming gate 2, P0 rule:
|