comfyui-mcp 0.52.114 → 0.52.116

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.
@@ -53,14 +53,9 @@ function progressIdentityOf(job) {
53
53
  * outcomes. Matching on id alone could annotate the wrong terminal, or miss a real
54
54
  * disagreement by finding the other one first (review).
55
55
  *
56
- * A target is compared only when BOTH sides carry one. Rows and records that predate the
57
- * field, or a route that never sets it, must not silently stop matching that would make
58
- * the check inert again, which is exactly how the first version shipped.
59
- *
60
- * PREFER THE EXACT (id, target) MATCH (review, round 3). Taking the first id match in
61
- * array order let a TARGETLESS record shadow the exact one: a targetless "downloading"
62
- * sitting before an exact-target terminal reported a disagreement that does not exist.
63
- * The targetless record still stands in when nothing matches on target.
56
+ * Target identity is production data, not a test-only discriminator. A row or record
57
+ * without it is ambiguous when local and pod transfers share an id, so legacy targetless
58
+ * records fail closed and never suppress or promote a terminal event.
64
59
  */
65
60
  function matchingRecord(row, jobs) {
66
61
  if (!row || typeof row !== "object")
@@ -70,18 +65,37 @@ function matchingRecord(row, jobs) {
70
65
  return null;
71
66
  if (!Array.isArray(jobs))
72
67
  return null;
73
- const target = typeof row.target === "string" ? row.target : null;
74
- const sameId = jobs.filter((j) => j && typeof j === "object" && progressIdentityOf(j) === id);
75
- if (!sameId.length)
68
+ const target = typeof row.target === "string" && row.target.length > 0 ? row.target : null;
69
+ if (!target)
70
+ return null;
71
+ const candidates = jobs.filter((j) => j &&
72
+ typeof j === "object" &&
73
+ progressIdentityOf(j) === id &&
74
+ typeof j.target === "string" &&
75
+ j.target.length > 0 &&
76
+ j.target === target);
77
+ if (!candidates.length)
76
78
  return null;
77
- return ((target ? sameId.find((j) => j.target === target) : undefined) ??
78
- sameId.find((j) => typeof j.target !== "string") ??
79
- (target ? undefined : sameId[0]) ??
79
+ // A reconnect can leave both an older terminal snapshot and the newer live record in
80
+ // the persisted store. `download_model action:"status"` resolves that ambiguity in
81
+ // favour of the live transfer; the tray guard must make the same choice or it can
82
+ // announce FAILED while status is still advancing.
83
+ // `download_model action:"status"` treats stale persisted rows as historical
84
+ // diagnostics and resolves the terminal record instead. Prefer a fresh live record
85
+ // when both snapshots are present, but keep a stale one available for error-detail
86
+ // lookup and explicit non-live handling below.
87
+ return (candidates.find((j) => j.status === "downloading" && j.staleInflight !== true) ??
88
+ candidates.find((j) => j.status !== "downloading") ??
89
+ candidates.find((j) => j.status === "downloading") ??
90
+ candidates[0] ??
80
91
  null);
81
92
  }
82
93
  function recordStillDownloading(row, jobs) {
83
94
  const record = matchingRecord(row, jobs);
84
- return record != null && record.status === "downloading";
95
+ // This is deliberately the same freshness boundary as the status action. A stale
96
+ // persisted heartbeat can remain on disk for six hours, but status resolves it as a
97
+ // terminal/no-live answer; it must not suppress a real FAILED tray notification.
98
+ return record != null && record.status === "downloading" && record.staleInflight !== true;
85
99
  }
86
100
  /**
87
101
  * Does the job record disagree with announcing this row as completed?
@@ -99,6 +113,19 @@ export function completionDisagreesWithRecord(row, jobs) {
99
113
  return false;
100
114
  return recordStillDownloading(row, jobs);
101
115
  }
116
+ /** Classify a terminal tray row against the exact production job identity. */
117
+ export function failureRecordDisposition(row, jobs) {
118
+ if (!row || typeof row !== "object" || row.status !== "error") {
119
+ return { disposition: "none", record: null };
120
+ }
121
+ const record = matchingRecord(row, jobs);
122
+ if (!record || record.status !== "downloading") {
123
+ return { disposition: "none", record: null };
124
+ }
125
+ if (record.staleInflight === true)
126
+ return { disposition: "stale", record };
127
+ return { disposition: row.progressAdvanced === true ? "advancing" : "stalled", record };
128
+ }
102
129
  /**
103
130
  * Does the job record disagree with announcing this row as FAILED?
104
131
  *
@@ -112,7 +139,47 @@ export function failureDisagreesWithRecord(row, jobs) {
112
139
  return false;
113
140
  if (row.status !== "error")
114
141
  return false;
115
- return recordStillDownloading(row, jobs);
142
+ // A fresh heartbeat says only that persistence happened. The failure hedge requires
143
+ // an actual byte increase observed by the poller, or a stalled stream could suppress
144
+ // a genuine FAILED notification indefinitely. Stalled/stale records are reconciled to
145
+ // terminal state by the production poll seam instead of being treated as live.
146
+ return failureRecordDisposition(row, jobs).disposition === "advancing";
147
+ }
148
+ /** Bound one error string before it reaches the tray/agent event. */
149
+ export function boundedDownloadError(value) {
150
+ if (typeof value !== "string")
151
+ return undefined;
152
+ const compact = value.replace(/\s+/g, " ").trim();
153
+ return compact ? compact.slice(0, 400) : undefined;
154
+ }
155
+ /** Return the bounded error detail from the authoritative terminal record, when present. */
156
+ export function failureErrorDetail(row, jobs) {
157
+ if (!row || typeof row !== "object" || row.status !== "error")
158
+ return undefined;
159
+ const rowError = boundedDownloadError(row.error);
160
+ const record = matchingRecord(row, jobs);
161
+ return (rowError ??
162
+ (record?.status !== "downloading" ? boundedDownloadError(record?.error) : undefined));
163
+ }
164
+ /**
165
+ * Reconcile the failure half of one production download_done batch.
166
+ *
167
+ * The poll loop calls this after it has read the tray rows and authoritative job records,
168
+ * before injecting the batch into the panel agent. Keeping the mutation here makes that
169
+ * production seam directly testable without treating PanelAgentManager injection as the
170
+ * producer-side proof.
171
+ */
172
+ export function reconcileDownloadDoneFailures(rows, jobs) {
173
+ const disagreeing = [];
174
+ for (const row of rows) {
175
+ if (failureDisagreesWithRecord(row, jobs)) {
176
+ row.recordDisagrees = true;
177
+ disagreeing.push(row);
178
+ }
179
+ if (row.status === "error")
180
+ row.error = failureErrorDetail(row, jobs);
181
+ }
182
+ return disagreeing;
116
183
  }
117
184
  /** The disclosure appended to a completion the record disagrees with. Deliberately states
118
185
  * BOTH readings and what to do, rather than picking a winner this cannot establish. */
@@ -1 +1 @@
1
- {"version":3,"file":"download-done-guard.js","sourceRoot":"","sources":["../../src/orchestrator/download-done-guard.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"download-done-guard.js","sourceRoot":"","sources":["../../src/orchestrator/download-done-guard.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import { listDownloadJobs, reconcileStalledDownloadRecord, } from "../services/download-jobs.js";
2
+ import { failureErrorDetail, failureRecordDisposition, reconcileDownloadDoneFailures, } from "./download-done-guard.js";
3
+ /**
4
+ * The failure-reconciliation stage used by the download poll loop.
5
+ *
6
+ * The live loop already has the authoritative records because the completion guard reads
7
+ * them once per flush; callers pass those records to avoid a second filesystem scan. Tests
8
+ * may omit them to exercise the same production lookup against the persisted job store.
9
+ */
10
+ export function reconcileDownloadDoneBatch(rows, records, progress, onInject) {
11
+ let authoritative = records;
12
+ if (!authoritative) {
13
+ try {
14
+ authoritative = listDownloadJobs();
15
+ }
16
+ catch {
17
+ authoritative = [];
18
+ }
19
+ }
20
+ for (const row of rows)
21
+ row.progressAdvanced = progress?.hasAdvanced(row) === true;
22
+ for (const row of rows) {
23
+ const match = failureRecordDisposition(row, authoritative);
24
+ if (match.disposition !== "stalled" && match.disposition !== "stale")
25
+ continue;
26
+ const durable = reconcileStalledDownloadRecord(match.record, failureErrorDetail(row, authoritative));
27
+ if (!durable)
28
+ row.recordDisagrees = true;
29
+ }
30
+ const disagreeing = reconcileDownloadDoneFailures(rows, authoritative);
31
+ // The advancement proof is producer-internal; recordDisagrees is the only reconciliation
32
+ // result the consumer needs. Do not add the polling detail to the agent event payload.
33
+ for (const row of rows)
34
+ delete row.progressAdvanced;
35
+ onInject?.({ kind: "download_done", downloads: rows });
36
+ return disagreeing;
37
+ }
38
+ //# sourceMappingURL=download-done-loop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"download-done-loop.js","sourceRoot":"","sources":["../../src/orchestrator/download-done-loop.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,8BAA8B,GAE/B,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACL,kBAAkB,EAClB,wBAAwB,EACxB,6BAA6B,GAE9B,MAAM,0BAA0B,CAAC;AAOlC;;;;;;GAMG;AACH,MAAM,UAAU,0BAA0B,CACxC,IAAS,EACT,OAAgC,EAChC,QAAmG,EACnG,QAAoD;IAEpD,IAAI,aAAa,GAAG,OAAO,CAAC;IAC5B,IAAI,CAAC,aAAa,EAAE,CAAC;QACnB,IAAI,CAAC;YACH,aAAa,GAAG,gBAAgB,EAAE,CAAC;QACrC,CAAC;QAAC,MAAM,CAAC;YACP,aAAa,GAAG,EAAE,CAAC;QACrB,CAAC;IACH,CAAC;IACD,KAAK,MAAM,GAAG,IAAI,IAAI;QAAE,GAAG,CAAC,gBAAgB,GAAG,QAAQ,EAAE,WAAW,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC;IACnF,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,KAAK,GAAG,wBAAwB,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC;QAC3D,IAAI,KAAK,CAAC,WAAW,KAAK,SAAS,IAAI,KAAK,CAAC,WAAW,KAAK,OAAO;YAAE,SAAS;QAC/E,MAAM,OAAO,GAAG,8BAA8B,CAC5C,KAAK,CAAC,MAAqB,EAC3B,kBAAkB,CAAC,GAAG,EAAE,aAAa,CAAC,CACvC,CAAC;QACF,IAAI,CAAC,OAAO;YAAE,GAAG,CAAC,eAAe,GAAG,IAAI,CAAC;IAC3C,CAAC;IACD,MAAM,WAAW,GAAG,6BAA6B,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;IACvE,yFAAyF;IACzF,uFAAuF;IACvF,KAAK,MAAM,GAAG,IAAI,IAAI;QAAE,OAAO,GAAG,CAAC,gBAAgB,CAAC;IACpD,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACvD,OAAO,WAAW,CAAC;AACrB,CAAC"}
@@ -50,7 +50,8 @@ import { startPanelImageRelayServer, verifyPanelImageRelayCapability, } from "..
50
50
  import { startPanelTemplateRelayServer, verifyPanelTemplateRelayCapability, } from "../services/panel-template-relay.js";
51
51
  import { logger } from "../utils/logger.js";
52
52
  import { listDownloadJobs } from "../services/download-jobs.js";
53
- import { completionDisagreesWithRecord, failureDisagreesWithRecord } from "./download-done-guard.js";
53
+ import { boundedDownloadError, completionDisagreesWithRecord, } from "./download-done-guard.js";
54
+ import { reconcileDownloadDoneBatch } from "./download-done-loop.js";
54
55
  import { assembleVocabularyHash, describeVocabularySkew } from "../tools/vocabulary.js";
55
56
  import { buildPanelToolDefs } from "./panel-tools.js";
56
57
  /** The panel vocabulary hashes whose MISMATCH has already been reported (#236).
@@ -573,9 +574,39 @@ export function pushModelsFrame(bridge, panelTabId, models, current, backend) {
573
574
  * hello/re-hello so a reconnect cannot retain rows from an old process.
574
575
  */
575
576
  export class DownloadProgressSnapshots {
577
+ /** A recent byte increase is required; heartbeat-only rows must not keep this hedge live. */
578
+ static ADVANCEMENT_MAX_AGE_MS = 5_000;
576
579
  lastSnapshot = null;
577
580
  rows = [];
581
+ progressByKey = new Map();
582
+ lastAdvancedAt = new Map();
583
+ progressKey(row) {
584
+ if (typeof row.id !== "string" || !row.id)
585
+ return null;
586
+ const target = typeof row.target === "string" ? row.target : "";
587
+ const attempt = typeof row.attempt === "number" ? String(row.attempt) : "";
588
+ return `${row.id}\n${target}\n${attempt}`;
589
+ }
578
590
  record(rows) {
591
+ for (const row of rows) {
592
+ if (row.status !== "downloading")
593
+ continue;
594
+ const key = this.progressKey(row);
595
+ const downloaded = typeof row.downloaded === "number" ? row.downloaded : undefined;
596
+ const total = typeof row.total === "number" ? row.total : undefined;
597
+ if (!key ||
598
+ downloaded === undefined ||
599
+ !Number.isFinite(downloaded) ||
600
+ downloaded < 0 ||
601
+ total === undefined ||
602
+ !Number.isFinite(total) ||
603
+ total < 0)
604
+ continue;
605
+ const previous = this.progressByKey.get(key);
606
+ if (previous && downloaded > previous.downloaded)
607
+ this.lastAdvancedAt.set(key, Date.now());
608
+ this.progressByKey.set(key, { downloaded, total });
609
+ }
579
610
  const snapshot = JSON.stringify(rows);
580
611
  if (snapshot === this.lastSnapshot)
581
612
  return false;
@@ -583,6 +614,15 @@ export class DownloadProgressSnapshots {
583
614
  this.rows = rows;
584
615
  return true;
585
616
  }
617
+ /** True only after a recent later progress row proves downloaded bytes increased. */
618
+ hasAdvanced(row, now = Date.now()) {
619
+ if (typeof row.id !== "string" || !row.id)
620
+ return false;
621
+ const target = typeof row.target === "string" ? row.target : "";
622
+ const attempt = typeof row.attempt === "number" ? String(row.attempt) : "";
623
+ const advancedAt = this.lastAdvancedAt.get(`${row.id}\n${target}\n${attempt}`);
624
+ return advancedAt !== undefined && now - advancedAt <= DownloadProgressSnapshots.ADVANCEMENT_MAX_AGE_MS;
625
+ }
586
626
  forPanel() {
587
627
  return this.rows;
588
628
  }
@@ -5472,6 +5512,13 @@ export async function runPanelOrchestrator() {
5472
5512
  }
5473
5513
  if (!row || typeof row !== "object")
5474
5514
  continue;
5515
+ if ("error" in row) {
5516
+ const error = boundedDownloadError(row.error);
5517
+ if (error)
5518
+ row.error = error;
5519
+ else
5520
+ delete row.error;
5521
+ }
5475
5522
  const updated = typeof row.updated === "number" ? row.updated : now;
5476
5523
  parsed.push({ full, row, status: row.status, updated });
5477
5524
  }
@@ -5553,6 +5600,7 @@ export async function runPanelOrchestrator() {
5553
5600
  target: typeof row.target === "string" ? row.target : undefined,
5554
5601
  name: String(row.name ?? row.id ?? "model"),
5555
5602
  status: String(status),
5603
+ error: boundedDownloadError(row.error),
5556
5604
  attempt: typeof row.attempt === "number" ? row.attempt : undefined,
5557
5605
  supKey,
5558
5606
  });
@@ -5645,9 +5693,9 @@ export async function runPanelOrchestrator() {
5645
5693
  // advancing. #1150 only sees a live TRAY row of that filename; here the tray
5646
5694
  // row itself was the error, so that hedge never ran. Flag it so the formatter
5647
5695
  // will not say FAILED.
5648
- const failedDisagreeing = settled.filter((d) => failureDisagreesWithRecord(d, records));
5649
- for (const d of failedDisagreeing)
5650
- d.recordDisagrees = true;
5696
+ const failedDisagreeing = reconcileDownloadDoneBatch(settled, records, downloadSnapshots, (event) => manager.injectEvent(key, event, {
5697
+ mid: turnOrigins.mintInheritedOrigin(),
5698
+ }));
5651
5699
  if (failedDisagreeing.length) {
5652
5700
  logger.warn("[panel-orchestrator] a download failure disagrees with the job record; disclosing rather than announcing FAILED (#2057)", { ids: failedDisagreeing.map((d) => String(d.id ?? "")) });
5653
5701
  }
@@ -5660,9 +5708,6 @@ export async function runPanelOrchestrator() {
5660
5708
  // ran — the turn routed to whatever tab was active (confirming gate 3,
5661
5709
  // P1). The minted mid contributes nothing and the batch close inherits
5662
5710
  // (or refuses, when no origin was ever established).
5663
- manager.injectEvent(key, { kind: "download_done", downloads: settled }, {
5664
- mid: turnOrigins.mintInheritedOrigin(),
5665
- });
5666
5711
  }
5667
5712
  // MCP-child control channel (#269): runpod_* tools that ran in spawned
5668
5713
  // agent children ask the orchestrator to retarget / watch / unwatch /