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.
- package/dist/orchestrator/codex-backend.js +303 -7
- package/dist/orchestrator/codex-backend.js.map +1 -1
- package/dist/orchestrator/panel-tools.js +163 -7
- package/dist/orchestrator/panel-tools.js.map +1 -1
- package/dist/services/panel-template-relay.js +180 -7
- package/dist/services/panel-template-relay.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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, {
|
|
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
|
-
|
|
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 } : {}) });
|