comfyui-mcp 0.52.136 → 0.52.138

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.
@@ -91,9 +91,12 @@ export function isCodexPanelMcpTransportFailure(message, panelUrl) {
91
91
  /** Keep a WorkerTransport failure self-contained: PanelAgent's generic turn
92
92
  * failure text says "Nothing was lost — try again", which is unsafe when a
93
93
  * mutation's dispatch/outcome was not established at this boundary. */
94
- function panelMcpTransportFailureNotice(message) {
94
+ function panelMcpTransportFailureNotice(message, retriedRead) {
95
+ const retryNote = retriedRead
96
+ ? `The orchestrator retried this read once, but that retry also failed; the original transport error is preserved.`
97
+ : `The orchestrator did not retry the request.`;
95
98
  return (`The local panel MCP connection failed before a tool result was returned. ` +
96
- `The orchestrator did not retry the request. For a render or other mutation, ` +
99
+ `${retryNote} For a render or other mutation, ` +
97
100
  `treat the first attempt's outcome as UNKNOWN until you inspect queue ` +
98
101
  `(action:"list") or get_history; do not re-run panel_run blindly. The panel MCP ` +
99
102
  `connection is being reloaded for the next turn. If it remains unavailable, ` +
@@ -575,6 +578,77 @@ export function toolNameOf(item) {
575
578
  // fileChange, webSearch, …) so the panel at least shows that a tool ran.
576
579
  return type ?? null;
577
580
  }
581
+ // A failed HTTP MCP send is observed outside panel-mcp-http.ts, so the backend
582
+ // has to classify the in-flight app-server item before deciding whether the
583
+ // transport may issue its one retry. Keep this allowlist intentionally narrower
584
+ // than the panel tool catalog: scope navigation and every mutation must never
585
+ // be replayed from an opaque outer transport error.
586
+ const PANEL_OUTER_TRANSPORT_RETRY_SAFE_TOOLS = new Set([
587
+ "panel_graph_outline",
588
+ "panel_query_graph",
589
+ ]);
590
+ function stringFieldOf(value, ...keys) {
591
+ if (!value)
592
+ return undefined;
593
+ for (const key of keys) {
594
+ if (typeof value[key] === "string" && value[key])
595
+ return value[key];
596
+ }
597
+ return undefined;
598
+ }
599
+ function isMcpToolCallItem(item) {
600
+ return stringFieldOf(item, "type") === "mcpToolCall";
601
+ }
602
+ function panelMcpToolItemOf(item) {
603
+ const qualified = toolNameOf(item);
604
+ if (!qualified)
605
+ return null;
606
+ const type = stringFieldOf(item, "type");
607
+ if (type && type !== "mcpToolCall")
608
+ return null;
609
+ const separator = qualified.lastIndexOf(".");
610
+ const server = separator >= 0 ? qualified.slice(0, separator) : undefined;
611
+ const name = separator >= 0 ? qualified.slice(separator + 1) : qualified;
612
+ // If app-server identifies the MCP server, require the configured panel
613
+ // server. An item id is required for transport correlation; the display path
614
+ // above still supports older items that only have a tool name.
615
+ if (server && server !== "panel")
616
+ return null;
617
+ const itemId = stringFieldOf(item, "id");
618
+ if (!itemId)
619
+ return null;
620
+ const requestKeys = new Set([itemId]);
621
+ for (const key of ["requestId", "request_id", "callId", "call_id"]) {
622
+ const value = stringFieldOf(item, key);
623
+ if (value)
624
+ requestKeys.add(value);
625
+ }
626
+ const retryOf = stringFieldOf(item, "retryOf", "retry_of", "parentItemId", "parent_item_id");
627
+ if (retryOf)
628
+ requestKeys.add(retryOf);
629
+ return {
630
+ name,
631
+ itemId,
632
+ requestKeys: [...requestKeys],
633
+ ...(retryOf ? { retryOf } : {}),
634
+ };
635
+ }
636
+ function panelMcpTransportErrorKeys(params) {
637
+ const error = params.error;
638
+ const details = error?.additionalDetails;
639
+ const item = params.item;
640
+ const keys = new Set();
641
+ for (const value of [
642
+ stringFieldOf(params, "itemId", "item_id", "requestId", "request_id", "callId", "call_id"),
643
+ stringFieldOf(item, "id", "requestId", "request_id", "callId", "call_id"),
644
+ stringFieldOf(error, "itemId", "item_id", "requestId", "request_id", "callId", "call_id"),
645
+ stringFieldOf(details, "itemId", "item_id", "requestId", "request_id", "callId", "call_id"),
646
+ ]) {
647
+ if (value)
648
+ keys.add(value);
649
+ }
650
+ return [...keys];
651
+ }
578
652
  /**
579
653
  * Read the current app-server status inventory without treating an absent
580
654
  * inventory as evidence. The current protocol uses a name -> schema map; the
@@ -1516,6 +1590,19 @@ export class CodexBackend {
1516
1590
  // the original panel mutation is still being retried.
1517
1591
  let mcpTransportFencePending = false;
1518
1592
  let turnFenced = false;
1593
+ const panelMcpRequests = new Map();
1594
+ const panelRequestKeyToItem = new Map();
1595
+ const panelReadRetries = new Map();
1596
+ const inFlightMcpItemIds = new Set();
1597
+ let unkeyedMcpItems = 0;
1598
+ let panelEnterCandidate = null;
1599
+ let immediatePanelReadAfterEnter = null;
1600
+ // Retry contract: only the next panel request after a successful
1601
+ // panel_enter_subgraph completion can be an eligible graph read. The
1602
+ // one-shot sequence is consumed at request start; a mutation or unrelated
1603
+ // panel request therefore invalidates it before a later read can qualify.
1604
+ // Correlation must resolve the transport error to that request; ambiguity
1605
+ // is fail-closed. Mutations and unrelated reads use normal fencing.
1519
1606
  // LIVENESS (watchdog re-arm): re-arm PanelAgent's idle watchdog ONLY for
1520
1607
  // notifications that represent work or an outcome for THIS active turn. A
1521
1608
  // long tool call streams item/* and turn/* notifications that carry no
@@ -1579,6 +1666,75 @@ export class CodexBackend {
1579
1666
  streamKind = null;
1580
1667
  }
1581
1668
  };
1669
+ // `willRetry:true` is the app-server's positive signal that it still owns
1670
+ // the active connection and can retry the failed MCP send. Keep the local
1671
+ // state checks as well: cancellation, timeout teardown, client replacement,
1672
+ // and disposal must not wake a retry after the turn has stopped being live.
1673
+ const appServerConnectionIsActive = () => !interrupted &&
1674
+ !finishedResult &&
1675
+ !turnFenced &&
1676
+ !this.disposed &&
1677
+ this.client === liveClient &&
1678
+ liveClient.exitError == null;
1679
+ const trackPanelMcpItem = (item, afterEnterItemId) => {
1680
+ const existing = panelMcpRequests.get(item.itemId);
1681
+ if (existing) {
1682
+ for (const key of item.requestKeys) {
1683
+ existing.requestKeys = [...new Set([...existing.requestKeys, key])];
1684
+ panelRequestKeyToItem.set(key, item.itemId);
1685
+ }
1686
+ return existing;
1687
+ }
1688
+ const request = {
1689
+ ...item,
1690
+ afterEnterItemId,
1691
+ completed: false,
1692
+ };
1693
+ panelMcpRequests.set(item.itemId, request);
1694
+ for (const key of item.requestKeys)
1695
+ panelRequestKeyToItem.set(key, item.itemId);
1696
+ return request;
1697
+ };
1698
+ const panelMcpRequestForTransportError = (params) => {
1699
+ const keys = panelMcpTransportErrorKeys(params);
1700
+ if (keys.length > 0) {
1701
+ const itemIds = new Set();
1702
+ let unknownKey = false;
1703
+ for (const key of keys) {
1704
+ const itemId = panelRequestKeyToItem.get(key) ?? (panelMcpRequests.has(key) ? key : undefined);
1705
+ if (itemId)
1706
+ itemIds.add(itemId);
1707
+ else
1708
+ unknownKey = true;
1709
+ }
1710
+ // An unknown explicit key must never fall back to a different request.
1711
+ if (unknownKey || itemIds.size !== 1)
1712
+ return null;
1713
+ const request = panelMcpRequests.get([...itemIds][0]);
1714
+ return request && !request.completed ? request : null;
1715
+ }
1716
+ // Older app-server errors omit itemId. A sole in-flight MCP request is
1717
+ // still unambiguous; any parallel or unkeyed MCP item makes it ineligible.
1718
+ if (unkeyedMcpItems > 0 || inFlightMcpItemIds.size !== 1)
1719
+ return null;
1720
+ const inFlight = [...panelMcpRequests.values()].filter((request) => !request.completed);
1721
+ if (inFlight.length !== 1)
1722
+ return null;
1723
+ const [itemId] = inFlightMcpItemIds;
1724
+ return inFlight[0].itemId === itemId ? inFlight[0] : null;
1725
+ };
1726
+ const panelReadRetryForRequest = (request) => {
1727
+ if (!request)
1728
+ return null;
1729
+ for (const [originItemId, retry] of panelReadRetries) {
1730
+ if (retry.itemIds.has(request.itemId) ||
1731
+ request.requestKeys.some((key) => retry.requestKeys.has(key))) {
1732
+ return { originItemId, retry };
1733
+ }
1734
+ }
1735
+ return null;
1736
+ };
1737
+ const firstPanelReadRetry = () => panelReadRetries.values().next().value;
1582
1738
  const abortActiveTurn = () => {
1583
1739
  interrupted = true;
1584
1740
  if (finishedResult)
@@ -1604,6 +1760,14 @@ export class CodexBackend {
1604
1760
  return;
1605
1761
  // Closing the old app-server is the point of a stale-host recycle;
1606
1762
  // its exit must not finish the replacement turn (#2045).
1763
+ const retry = firstPanelReadRetry();
1764
+ if (retry) {
1765
+ panelReadRetries.clear();
1766
+ emitTerminalError(panelMcpTransportFailureNotice(retry.originalMessage, true), {
1767
+ outcomeUnknown: true,
1768
+ });
1769
+ return;
1770
+ }
1607
1771
  if (hostRecycleInFlight)
1608
1772
  return;
1609
1773
  emitTerminalError(watched.exitError ? msgOf(watched.exitError) : "codex app-server connection closed.");
@@ -1617,14 +1781,14 @@ export class CodexBackend {
1617
1781
  }
1618
1782
  await this.beginClientClose(fencedClient, "panel MCP transport recovery fence teardown failed");
1619
1783
  };
1620
- const finishMcpTransportFailure = (message, willRetry) => {
1784
+ const finishMcpTransportFailure = (message, willRetry, retriedRead = false) => {
1621
1785
  if (mcpTransportFencePending || finishedResult)
1622
1786
  return;
1623
1787
  mcpTransportFencePending = true;
1624
1788
  // willRetry:false is already terminal at the app-server boundary. Keep
1625
1789
  // the existing bounded recovery path and its single local error/result.
1626
1790
  if (!willRetry) {
1627
- emitTerminalError(panelMcpTransportFailureNotice(message), { outcomeUnknown: true });
1791
+ emitTerminalError(panelMcpTransportFailureNotice(message, retriedRead), { outcomeUnknown: true });
1628
1792
  return;
1629
1793
  }
1630
1794
  // willRetry:true is different: Codex may still retry the failed MCP call.
@@ -1667,7 +1831,9 @@ export class CodexBackend {
1667
1831
  const fenceNote = interruptError
1668
1832
  ? " The active app-server turn could not be interrupted; its connection was closed before recovery."
1669
1833
  : " The active app-server turn was interrupted before recovery.";
1670
- emitTerminalError(panelMcpTransportFailureNotice(message) + fenceNote, { outcomeUnknown: true });
1834
+ emitTerminalError(panelMcpTransportFailureNotice(message, retriedRead) + fenceNote, {
1835
+ outcomeUnknown: true,
1836
+ });
1671
1837
  })();
1672
1838
  };
1673
1839
  const tryRetryCodeModeHostSpawn = (message) => {
@@ -1815,6 +1981,43 @@ export class CodexBackend {
1815
1981
  // reasoning items aren't "tools" — they're handled by the delta/commit
1816
1982
  // paths above — so skip them here.
1817
1983
  const item = params.item;
1984
+ if (isMcpToolCallItem(item)) {
1985
+ const itemId = stringFieldOf(item, "id");
1986
+ if (itemId)
1987
+ inFlightMcpItemIds.add(itemId);
1988
+ else
1989
+ unkeyedMcpItems += 1;
1990
+ }
1991
+ const panelTool = panelMcpToolItemOf(item);
1992
+ if (panelTool) {
1993
+ const isNewPanelRequest = !panelMcpRequests.has(panelTool.itemId);
1994
+ let afterEnterItemId = null;
1995
+ if (isNewPanelRequest) {
1996
+ if (panelTool.name === "panel_enter_subgraph") {
1997
+ immediatePanelReadAfterEnter = null;
1998
+ panelEnterCandidate = { itemId: panelTool.itemId, laterPanelRequestStarted: false };
1999
+ }
2000
+ else {
2001
+ if (panelEnterCandidate)
2002
+ panelEnterCandidate.laterPanelRequestStarted = true;
2003
+ if (immediatePanelReadAfterEnter && PANEL_OUTER_TRANSPORT_RETRY_SAFE_TOOLS.has(panelTool.name)) {
2004
+ afterEnterItemId = immediatePanelReadAfterEnter;
2005
+ }
2006
+ immediatePanelReadAfterEnter = null;
2007
+ }
2008
+ }
2009
+ const request = trackPanelMcpItem(panelTool, afterEnterItemId);
2010
+ for (const [originItemId, retry] of panelReadRetries) {
2011
+ if (request.name === retry.tool &&
2012
+ (request.requestKeys.some((key) => retry.requestKeys.has(key)) ||
2013
+ request.retryOf === originItemId)) {
2014
+ retry.itemIds.add(request.itemId);
2015
+ for (const key of request.requestKeys)
2016
+ retry.requestKeys.add(key);
2017
+ break;
2018
+ }
2019
+ }
2020
+ }
1818
2021
  const name = toolNameOf(item);
1819
2022
  if (name)
1820
2023
  push({ type: "tool_call", name, phase: "start", detail: item });
@@ -1825,7 +2028,46 @@ export class CodexBackend {
1825
2028
  // (reasoning OR reply) then emit the canonical event: `assistant` for an
1826
2029
  // agentMessage, or tool_call(end) for a finished tool/command/MCP item.
1827
2030
  const item = params.item;
2031
+ if (isMcpToolCallItem(item)) {
2032
+ const itemId = stringFieldOf(item, "id");
2033
+ if (itemId)
2034
+ inFlightMcpItemIds.delete(itemId);
2035
+ else if (unkeyedMcpItems > 0)
2036
+ unkeyedMcpItems -= 1;
2037
+ }
1828
2038
  const itemType = item?.type;
2039
+ const panelTool = panelMcpToolItemOf(item);
2040
+ const request = panelTool ? panelMcpRequests.get(panelTool.itemId) : undefined;
2041
+ if (request) {
2042
+ request.completed = true;
2043
+ const completedSuccessfully = item?.status !== "failed" &&
2044
+ item?.status !== "error" &&
2045
+ item?.status !== "declined" &&
2046
+ item?.error == null;
2047
+ if (request.name === "panel_enter_subgraph") {
2048
+ immediatePanelReadAfterEnter =
2049
+ completedSuccessfully &&
2050
+ panelEnterCandidate?.itemId === request.itemId &&
2051
+ !panelEnterCandidate.laterPanelRequestStarted
2052
+ ? request.itemId
2053
+ : null;
2054
+ panelEnterCandidate = null;
2055
+ }
2056
+ const retryMatch = panelReadRetryForRequest(request);
2057
+ if (retryMatch && request.name === retryMatch.retry.tool) {
2058
+ const retryMessage = retryMatch.retry.originalMessage;
2059
+ panelReadRetries.delete(retryMatch.originItemId);
2060
+ if (!completedSuccessfully) {
2061
+ finishMcpTransportFailure(retryMessage, false, true);
2062
+ break;
2063
+ }
2064
+ // A successful completion proves that this request's one retry
2065
+ // produced a tool result; do not queue the control-plane reload
2066
+ // for the transient error recovered in-band.
2067
+ if (panelReadRetries.size === 0)
2068
+ this.mcpTransportRecovery.delete("panel");
2069
+ }
2070
+ }
1829
2071
  closeStream();
1830
2072
  if (itemType === "agentMessage") {
1831
2073
  // `item.text` is normally a string, but newer app-server builds can
@@ -1870,7 +2112,45 @@ export class CodexBackend {
1870
2112
  // panel transport failure for the turn-boundary reload, and make the
1871
2113
  // outcome/reconnect boundary explicit to the caller.
1872
2114
  if (this.noteMcpTransportFailure(message)) {
1873
- finishMcpTransportFailure(message, params.willRetry === true);
2115
+ const request = panelMcpRequestForTransportError(params);
2116
+ const matchedRetry = panelReadRetryForRequest(request);
2117
+ // Only a correlated terminal error for the armed request is its
2118
+ // retry outcome. A distinct mutation or unrelated request never
2119
+ // borrows, clears, or consumes that read's retry.
2120
+ const retryAttempted = matchedRetry?.retry;
2121
+ if (!retryAttempted &&
2122
+ params.willRetry === true &&
2123
+ request != null &&
2124
+ request.afterEnterItemId != null &&
2125
+ PANEL_OUTER_TRANSPORT_RETRY_SAFE_TOOLS.has(request.name) &&
2126
+ appServerConnectionIsActive()) {
2127
+ // The Codex app-server owns the actual HTTP client. Let its live
2128
+ // connection perform this one retry, but remember the first error
2129
+ // so a second failure cannot amplify or replace the diagnosis.
2130
+ panelReadRetries.set(request.itemId, {
2131
+ originalMessage: message,
2132
+ tool: request.name,
2133
+ itemIds: new Set([request.itemId]),
2134
+ requestKeys: new Set(request.requestKeys),
2135
+ });
2136
+ break;
2137
+ }
2138
+ // A transport error for a different request must not consume or
2139
+ // rewrite a read retry that is already armed. The mutation still
2140
+ // fences this turn, because its dispatch/outcome is unknown, but
2141
+ // retain the saved read error in the terminal diagnosis and leave
2142
+ // its per-item retry state untouched.
2143
+ const pendingRead = firstPanelReadRetry();
2144
+ const failureMessage = pendingRead
2145
+ ? `${pendingRead.originalMessage}\nA distinct panel MCP request also failed: ${message}`
2146
+ : message;
2147
+ finishMcpTransportFailure(retryAttempted?.originalMessage ?? failureMessage, params.willRetry === true, retryAttempted !== undefined);
2148
+ break;
2149
+ }
2150
+ if (panelReadRetries.size > 0 && params.willRetry !== true) {
2151
+ const retry = firstPanelReadRetry();
2152
+ panelReadRetries.clear();
2153
+ finishMcpTransportFailure(retry?.originalMessage ?? message, false, retry !== undefined);
1874
2154
  break;
1875
2155
  }
1876
2156
  if (params.willRetry === true)
@@ -1886,8 +2166,24 @@ export class CodexBackend {
1886
2166
  break;
1887
2167
  }
1888
2168
  case "turn/completed": {
1889
- closeStream();
1890
2169
  const t = params.turn;
2170
+ if (panelReadRetries.size > 0 && (t?.status === "interrupted" || interrupted)) {
2171
+ // User cancellation (including a watchdog timeout that successfully
2172
+ // interrupts the turn) is not a failed retry. Preserve the normal
2173
+ // interrupted result and let the existing turn-end recovery observe
2174
+ // the transport episode on its usual path.
2175
+ panelReadRetries.clear();
2176
+ }
2177
+ else if (panelReadRetries.size > 0) {
2178
+ const retry = firstPanelReadRetry();
2179
+ panelReadRetries.clear();
2180
+ // A turn completion without a successful retry item is the retry's
2181
+ // terminal outcome. Preserve the original transport error rather
2182
+ // than reporting a later wrapper or a false success.
2183
+ finishMcpTransportFailure(retry?.originalMessage ?? "panel MCP read retry failed", false, true);
2184
+ break;
2185
+ }
2186
+ closeStream();
1891
2187
  // Mark a result emitted so a racing terminal-error path stays a no-op.
1892
2188
  finishedResult = true;
1893
2189
  push({ type: "result", ok: t?.status === "completed", ...(t?.status ? { subtype: t.status } : {}) });