comfyui-mcp 0.52.126 → 0.52.128

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.
@@ -134,6 +134,7 @@ import { readComfyuiCrashLog, formatCrashNote } from "../services/crash-log.js";
134
134
  import { QueueMonitor } from "../services/queue-monitor.js";
135
135
  import { formatQueueNote } from "./queue-note.js";
136
136
  import { RunCompletions, describe as describeCorrelation, } from "./run-completion-journal.js";
137
+ import { RunCompletionIdempotencyFence, scheduleRunCompletion, } from "./run-completion-idempotency.js";
137
138
  import { createRunCompletionWatchdog, resolveHistoryCompletionImages, resolveHistoryCompletionStatus, } from "./run-completion-watchdog.js";
138
139
  import { AskAnswers, preview as previewQuestion } from "./ask-answer-journal.js";
139
140
  import { initRunpodWatcher, getRunpodWatcher } from "../services/runpod-watch.js";
@@ -721,6 +722,9 @@ export function armStartupDeadline(port, deps = {}) {
721
722
  return () => clearTimeout(timer);
722
723
  }
723
724
  export async function runPanelOrchestrator() {
725
+ const completionFence = new RunCompletionIdempotencyFence();
726
+ /** Stable completion keys held by accepted queue items until the real turn ack. */
727
+ const completionFenceTokens = new Map();
724
728
  // Crash guard: the orchestrator is a long-lived background process the user
725
729
  // can't see. A stray rejection (e.g. a fire-and-forget push to a tab that
726
730
  // vanished mid-flight, or an SDK hiccup) must never silently kill it —
@@ -2760,11 +2764,22 @@ export async function runPanelOrchestrator() {
2760
2764
  }
2761
2765
  function flushRunCompletions(panelTabId) {
2762
2766
  const key = agentKeyFor(panelTabId);
2763
- const { blockedOn } = RunCompletions.deliverPending(panelTabId, (payload, token) =>
2764
- // #884 P0 — the injected turn carries the completion's ORIGIN tab, so it
2765
- // pins/stamps there (show the render on the tab that ran it), never on
2766
- // whatever tab is active (confirming-gate 2).
2767
- manager.injectEvent(key, payload, { eventToken: token, mid: turnOrigins.mintInjectionOrigin(panelTabId) }));
2767
+ const { blockedOn } = RunCompletions.deliverPending(panelTabId, (payload, token) => scheduleRunCompletion({
2768
+ route: key,
2769
+ payload,
2770
+ token,
2771
+ fence: completionFence,
2772
+ // #884 P0 — the injected turn carries the completion's ORIGIN tab, so it
2773
+ // pins/stamps there (show the render on the tab that ran it), never on
2774
+ // whatever tab is active (confirming-gate 2).
2775
+ inject: () => manager.injectEvent(key, payload, {
2776
+ eventToken: token,
2777
+ mid: turnOrigins.mintInjectionOrigin(panelTabId),
2778
+ }),
2779
+ onAccepted: (identity) => completionFenceTokens.set(token, identity),
2780
+ suppress: (duplicateToken) => RunCompletions.suppress(duplicateToken),
2781
+ log: (message) => logger.info(`[panel-orchestrator] tab ${panelTabId.slice(0, 8)} ${message} before creating another agent turn (#2341)`),
2782
+ }));
2768
2783
  if (blockedOn) {
2769
2784
  logger.warn(`[panel-orchestrator] tab ${panelTabId.slice(0, 8)} has no live agent for ${describeCorrelation(blockedOn.correlation)} — journaled, replayed when one comes back (#468)`);
2770
2785
  }
@@ -2830,16 +2845,33 @@ export async function runPanelOrchestrator() {
2830
2845
  // #486 — the ask journal verifies WHICH agent instance is acking, so a
2831
2846
  // provider switch cannot let the new conversation certify the old one's
2832
2847
  // answer. Run completions have their own turn-marker gate and need none.
2833
- if (token.startsWith("aa"))
2848
+ if (token.startsWith("aa")) {
2834
2849
  AskAnswers.ack(token, from);
2835
- else
2836
- RunCompletions.ack(token);
2850
+ return;
2851
+ }
2852
+ const identity = completionFenceTokens.get(token);
2853
+ if (identity && !completionFence.markDelivered(identity)) {
2854
+ logger.warn(`[panel-orchestrator] could not persist the delivered completion fence for ${token}; a restart may replay it (#2341)`);
2855
+ }
2856
+ if (identity)
2857
+ completionFenceTokens.delete(token);
2858
+ RunCompletions.ack(token);
2837
2859
  }
2838
2860
  function releaseEventToken(token, carried) {
2839
- if (token.startsWith("aa"))
2861
+ if (token.startsWith("aa")) {
2840
2862
  AskAnswers.release(token, { carried });
2841
- else
2842
- RunCompletions.release(token, { carried });
2863
+ return;
2864
+ }
2865
+ // Release the scheduling reservation BEFORE re-arming the journal. The
2866
+ // journal's release hook immediately flushes a replacement agent, so the
2867
+ // replay must be able to reclaim the identity in the same call stack.
2868
+ const identity = completionFenceTokens.get(token);
2869
+ if (identity && !completionFence.release(identity)) {
2870
+ logger.warn(`[panel-orchestrator] could not release the completion fence for ${token}; replay remains durability-blocked (#2341)`);
2871
+ }
2872
+ if (identity)
2873
+ completionFenceTokens.delete(token);
2874
+ RunCompletions.release(token, { carried });
2843
2875
  }
2844
2876
  // Flag the mobile mirror picker's "session attached" (green) dot from live agents.
2845
2877
  bridge.setHasSessionPredicate((tabId) => manager.hasLiveAgent(agentKeyFor(tabId)));