comfyui-mcp 0.48.30 → 0.48.32
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/index.js +74 -5
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/orchestrator/panel-tools.js +244 -5
- package/dist/orchestrator/panel-tools.js.map +1 -1
- package/dist/services/download-cache.js +191 -47
- package/dist/services/download-cache.js.map +1 -1
- package/dist/services/download-progress.js +75 -3
- package/dist/services/download-progress.js.map +1 -1
- package/dist/services/model-resolver.js +33 -2
- package/dist/services/model-resolver.js.map +1 -1
- package/dist/services/node-authoring.js +17 -8
- package/dist/services/node-authoring.js.map +1 -1
- package/package.json +1 -1
- package/plugin/skills/report-bug/SKILL.md +21 -7
|
@@ -59,7 +59,7 @@ import { readComfyuiCrashLog, formatCrashNote } from "../services/crash-log.js";
|
|
|
59
59
|
import { QueueMonitor } from "../services/queue-monitor.js";
|
|
60
60
|
import { initRunpodWatcher, getRunpodWatcher } from "../services/runpod-watch.js";
|
|
61
61
|
import { getPod } from "../services/runpod-client.js";
|
|
62
|
-
import { listTargetChangeRequests, consumeTargetChange, ackTargetChange, setProgressDir, CONTROL_PREFIX } from "../services/download-progress.js";
|
|
62
|
+
import { listTargetChangeRequests, consumeTargetChange, ackTargetChange, setProgressDir, CONTROL_PREFIX, newestAttemptEpochs, isSupersededAttempt, downloadAttemptKey } from "../services/download-progress.js";
|
|
63
63
|
import { hasActiveTrainingJob, reconcileStaleTrainingJobs } from "../services/training-jobs.js";
|
|
64
64
|
import { buildQueueStatusFrame, createQueueStatusBroadcaster, } from "../services/queue-status-broadcast.js";
|
|
65
65
|
import { unloadAllOllama, warmOllama, resolveOllamaHost } from "../services/ollama-vram.js";
|
|
@@ -3453,6 +3453,9 @@ export async function runPanelOrchestrator() {
|
|
|
3453
3453
|
// at which we emit. Keyed by download IDENTITY (row.id), not display name, so
|
|
3454
3454
|
// two distinct downloads sharing a filename in one batch don't overwrite each
|
|
3455
3455
|
// other and hide a failure (codex).
|
|
3456
|
+
// Each pending terminal carries its attempt epoch + (id,target) supersession key so a
|
|
3457
|
+
// newer live attempt for the SAME logical download can evict a queued FAILED/done
|
|
3458
|
+
// before it fires (panel#489).
|
|
3456
3459
|
const downloadDonePending = new Map();
|
|
3457
3460
|
// Resolve which agent to wake for a settled download row: the stamped tab's
|
|
3458
3461
|
// agent when it's still live; else the SINGLE live agent (pre-fix/in-process
|
|
@@ -3559,7 +3562,10 @@ export async function runPanelOrchestrator() {
|
|
|
3559
3562
|
files = []; // dir not created yet — nothing downloading
|
|
3560
3563
|
}
|
|
3561
3564
|
const now = Date.now();
|
|
3562
|
-
|
|
3565
|
+
// Phase 1: parse every tray row up front (skip control-channel + corrupt files),
|
|
3566
|
+
// so the attempt-supersession map (panel#489) is computed over the WHOLE tray
|
|
3567
|
+
// before any terminal row is acted on.
|
|
3568
|
+
const parsed = [];
|
|
3563
3569
|
for (const f of files) {
|
|
3564
3570
|
if (f.startsWith(CONTROL_PREFIX))
|
|
3565
3571
|
continue; // control channel, not a download row
|
|
@@ -3573,8 +3579,57 @@ export async function runPanelOrchestrator() {
|
|
|
3573
3579
|
}
|
|
3574
3580
|
if (!row || typeof row !== "object")
|
|
3575
3581
|
continue;
|
|
3576
|
-
const status = row.status;
|
|
3577
3582
|
const updated = typeof row.updated === "number" ? row.updated : now;
|
|
3583
|
+
parsed.push({ full, row, status: row.status, updated });
|
|
3584
|
+
}
|
|
3585
|
+
// Phase 2: newest attempt epoch per (id, target) (panel#489). A same-URL retry
|
|
3586
|
+
// reuses the deterministic id, so this distinguishes attempt N+1 from attempt N's
|
|
3587
|
+
// abandoned transfer — while the (id, target) scope keeps a concurrent LOCAL and
|
|
3588
|
+
// POD download of the same URL (#269) independent (neither supersedes the other).
|
|
3589
|
+
// Dead "downloading" writers (>60s stale) are excluded so a crashed attempt can't
|
|
3590
|
+
// shadow a real terminal; terminal rows are always kept (the newer attempt that
|
|
3591
|
+
// supersedes may itself have already finished).
|
|
3592
|
+
const freshForAttempts = parsed
|
|
3593
|
+
.filter((p) => p.status !== "downloading" || now - p.updated <= 60000)
|
|
3594
|
+
.map((p) => p.row);
|
|
3595
|
+
const newestAttemptByKey = newestAttemptEpochs(freshForAttempts);
|
|
3596
|
+
// A newer attempt for an (id, target) invalidates any terminal event STILL PENDING
|
|
3597
|
+
// for that same download from an abandoned attempt (the cross-tick case: attempt N's
|
|
3598
|
+
// terminal was observed and queued on an earlier poll, then attempt N+1's row
|
|
3599
|
+
// appeared). Evict the superseded entry from every agent's debounce bucket so no
|
|
3600
|
+
// "download FAILED" turn fires against a download that is actually still progressing.
|
|
3601
|
+
for (const bucket of downloadDonePending.values()) {
|
|
3602
|
+
for (const [idKey, entry] of bucket.downloads) {
|
|
3603
|
+
if (entry.attempt === undefined)
|
|
3604
|
+
continue;
|
|
3605
|
+
const newest = newestAttemptByKey.get(entry.supKey);
|
|
3606
|
+
if (newest !== undefined && newest > entry.attempt)
|
|
3607
|
+
bucket.downloads.delete(idKey);
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
const downloads = [];
|
|
3611
|
+
for (const { full, row, status, updated } of parsed) {
|
|
3612
|
+
// panel#489: a row from a SUPERSEDED attempt (older `attempt` epoch than the newest
|
|
3613
|
+
// attempt for this (id, target)) is a late artifact of the abandoned attempt a retry
|
|
3614
|
+
// replaced. Drop it entirely — a superseded TERMINAL fires no FAILED/done agent
|
|
3615
|
+
// event, and a superseded "downloading" row is kept off the tray + idle-stop veto so
|
|
3616
|
+
// it can't contradict or duplicate the live retry. Re-read before unlinking so a
|
|
3617
|
+
// writer that replaced the file between the parse above and here isn't clobbered
|
|
3618
|
+
// (only remove a file that STILL belongs to a superseded attempt). Per-attempt files
|
|
3619
|
+
// mean the retry writes a DIFFERENT file, so both coexist and this is deterministic
|
|
3620
|
+
// — never a shared-file race. A genuinely-current row (no newer attempt) is
|
|
3621
|
+
// unaffected and still shows/emits.
|
|
3622
|
+
if (isSupersededAttempt(row, newestAttemptByKey)) {
|
|
3623
|
+
try {
|
|
3624
|
+
const cur = JSON.parse(readFileSync(full, "utf8"));
|
|
3625
|
+
if (isSupersededAttempt(cur, newestAttemptByKey)) {
|
|
3626
|
+
unlinkSync(full);
|
|
3627
|
+
downloadRemoveAt.delete(full);
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
catch { /* gone or mid-write — nothing to remove */ }
|
|
3631
|
+
continue;
|
|
3632
|
+
}
|
|
3578
3633
|
if (status === "done" || status === "error") {
|
|
3579
3634
|
const due = downloadRemoveAt.get(full);
|
|
3580
3635
|
if (due == null) {
|
|
@@ -3587,8 +3642,17 @@ export async function runPanelOrchestrator() {
|
|
|
3587
3642
|
if (key && manager.hasLiveAgent(key)) {
|
|
3588
3643
|
const bucket = downloadDonePending.get(key) ??
|
|
3589
3644
|
{ downloads: new Map(), flushAt: 0 };
|
|
3590
|
-
|
|
3591
|
-
|
|
3645
|
+
// Identify each pending download by its (id, target) supersession key — NOT
|
|
3646
|
+
// the id alone: a concurrent LOCAL + POD transfer of the same URL shares an id
|
|
3647
|
+
// but must produce TWO #547 outcomes, and the same key lets a newer attempt
|
|
3648
|
+
// evict this entry above. Fall back to the file path when the row has no id.
|
|
3649
|
+
const supKey = downloadAttemptKey(row) ?? ` ${full}`;
|
|
3650
|
+
bucket.downloads.set(supKey, {
|
|
3651
|
+
name: String(row.name ?? row.id ?? "model"),
|
|
3652
|
+
status: String(status),
|
|
3653
|
+
attempt: typeof row.attempt === "number" ? row.attempt : undefined,
|
|
3654
|
+
supKey,
|
|
3655
|
+
});
|
|
3592
3656
|
bucket.flushAt = now + DOWNLOAD_DONE_DEBOUNCE_MS;
|
|
3593
3657
|
downloadDonePending.set(key, bucket);
|
|
3594
3658
|
}
|
|
@@ -3633,6 +3697,11 @@ export async function runPanelOrchestrator() {
|
|
|
3633
3697
|
continue;
|
|
3634
3698
|
downloadDonePending.delete(key);
|
|
3635
3699
|
const settled = [...bucket.downloads.values()];
|
|
3700
|
+
// The bucket can be emptied by the supersession eviction above (panel#489) —
|
|
3701
|
+
// a queued terminal cancelled by a newer live attempt. Don't fire an empty
|
|
3702
|
+
// "download_done" turn in that case.
|
|
3703
|
+
if (settled.length === 0)
|
|
3704
|
+
continue;
|
|
3636
3705
|
manager.injectEvent(key, { kind: "download_done", downloads: settled });
|
|
3637
3706
|
}
|
|
3638
3707
|
// MCP-child control channel (#269): runpod_* tools that ran in spawned
|