comfyui-mcp 0.52.138 → 0.52.140
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/panel-tools.js +262 -9
- package/dist/orchestrator/panel-tools.js.map +1 -1
- package/dist/services/api-nodes.js +22 -0
- package/dist/services/api-nodes.js.map +1 -1
- package/dist/services/model-inventory-disclosure.js +78 -0
- package/dist/services/model-inventory-disclosure.js.map +1 -0
- package/dist/services/model-resolver.js +5 -15
- package/dist/services/model-resolver.js.map +1 -1
- package/dist/services/node-dev.js +41 -1
- package/dist/services/node-dev.js.map +1 -1
- package/package.json +1 -1
- package/scripts/check-changelog.mjs +685 -0
- package/scripts/lib/changelog-refs.mjs +149 -0
- package/scripts/lib/release-subject.mjs +13 -1
- package/scripts/run-checks.mjs +10 -0
|
@@ -34,6 +34,7 @@ import { fileURLToPath } from "node:url";
|
|
|
34
34
|
import { comfyuiFetch } from "../comfyui/fetch.js";
|
|
35
35
|
import { assertPanelNotTargetedUnverifiable } from "../services/panel-pin-guard.js";
|
|
36
36
|
import { findPackOnDisk, nodesInstallCommandArgs, } from "../services/node-management.js";
|
|
37
|
+
import { getModelInventoryDisclosure, } from "../services/model-inventory-disclosure.js";
|
|
37
38
|
import { sanitizePanelUpdateNodeResult } from "../services/manager-update-error.js";
|
|
38
39
|
import { formatQueueStatusPartialNote, getManifestPartialLeftover, } from "../services/manifest-partial.js";
|
|
39
40
|
import { searchPanelNodes } from "../services/manager-node-search.js";
|
|
@@ -4452,6 +4453,25 @@ function withSaveAsNameConflictNote(res) {
|
|
|
4452
4453
|
return res;
|
|
4453
4454
|
return appendToolResultText(res, SAVE_AS_NAME_CONFLICT_NOTE);
|
|
4454
4455
|
}
|
|
4456
|
+
/**
|
|
4457
|
+
* #2414 — a live `/models/<category>` hit does not reconcile the panel's
|
|
4458
|
+
* `/object_info` combo cache. Add the distinction only when both sides are
|
|
4459
|
+
* positively observed; an unavailable listing remains an ordinary refusal.
|
|
4460
|
+
*/
|
|
4461
|
+
async function withModelInventoryDisclosure(res, widget, value, ctx) {
|
|
4462
|
+
if (!res.isError)
|
|
4463
|
+
return res;
|
|
4464
|
+
const probe = ctx.modelInventoryDisclosure ?? getModelInventoryDisclosure;
|
|
4465
|
+
try {
|
|
4466
|
+
const disclosure = await probe(widget, value, textOfToolResult(res));
|
|
4467
|
+
return disclosure ? appendToolResultText(res, disclosure) : res;
|
|
4468
|
+
}
|
|
4469
|
+
catch {
|
|
4470
|
+
// The disclosure is advisory. Preserve the original panel refusal if its
|
|
4471
|
+
// read-only corroboration fails for any reason.
|
|
4472
|
+
return res;
|
|
4473
|
+
}
|
|
4474
|
+
}
|
|
4455
4475
|
// ---- #1695: bound the panel_set_widget previous/new echo --------------------
|
|
4456
4476
|
//
|
|
4457
4477
|
// Code-mode clients (Codex `functions.exec`) batch several panel_set_widget
|
|
@@ -5181,11 +5201,41 @@ function promotedEnvelopeCarriesEvidence(payload) {
|
|
|
5181
5201
|
/** The shipped panel's only definitive non-promoted result is its own
|
|
5182
5202
|
* `Node <id> (<type>) is not a subgraph` refusal. Every other graph_get_subgraph
|
|
5183
5203
|
* error is an indeterminate read: a disconnect, stale route, old panel, or
|
|
5184
|
-
* transport failure can all leave a promoted container unresolved.
|
|
5204
|
+
* transport failure can all leave a promoted container unresolved.
|
|
5205
|
+
*
|
|
5206
|
+
* #2394 — the parenthesised segment is the node TYPE, and a node type may itself
|
|
5207
|
+
* contain parentheses: rgthree names every node that way (`Power Lora Loader
|
|
5208
|
+
* (rgthree)`), as do several other packs (`KSampler (Efficient)`). A `[^)]*`
|
|
5209
|
+
* body stops at the FIRST `)`, so `Node 82 (Power Lora Loader (rgthree)) is not
|
|
5210
|
+
* a subgraph` did not match its own definitive message. The read was then
|
|
5211
|
+
* treated as indeterminate and the write refused with "graph_get_subgraph could
|
|
5212
|
+
* not determine whether the addressed node is a promoted container" — the exact
|
|
5213
|
+
* refusal #2394 reported, on a node that is provably ordinary.
|
|
5214
|
+
*
|
|
5215
|
+
* The type body is matched as a BALANCED group (plain text, or text containing
|
|
5216
|
+
* one parenthesised group) UNIONED with the original `[^)]*` branch. Both halves
|
|
5217
|
+
* of that union are load-bearing, and each was added because a simpler edit
|
|
5218
|
+
* regressed a case that already worked (both caught by the codex gate):
|
|
5219
|
+
*
|
|
5220
|
+
* - Widening to `.*` looks equivalent but is not: `.` does not cross a newline
|
|
5221
|
+
* and `[^)]*` did, so a node type carrying a newline stopped matching.
|
|
5222
|
+
* - The balanced branch alone rejects an UNMATCHED `(` — a type like
|
|
5223
|
+
* `Power (rgthree` yields `Node 82 (Power (rgthree) is not a subgraph`, which
|
|
5224
|
+
* the original matched by stopping at the first `)`.
|
|
5225
|
+
*
|
|
5226
|
+
* Keeping the original branch in the union makes the accept set a strict
|
|
5227
|
+
* superset of the original's, so no message that was definitive before can
|
|
5228
|
+
* become indeterminate now. The reject set is unchanged: still anchored at
|
|
5229
|
+
* `^Error: Node `, still requiring the literal ` is not a subgraph` tail. A type
|
|
5230
|
+
* nesting parentheses more than one level deep still does not match and is
|
|
5231
|
+
* refused as indeterminate — the fail-closed direction, which is the correct
|
|
5232
|
+
* default for a predicate whose `true` authorizes an ordinary write. The
|
|
5233
|
+
* `[canvas-root-divergence]` diagnosis never begins with `Error: Node `, so it
|
|
5234
|
+
* cannot collide. */
|
|
5185
5235
|
function isDefinitiveNonPromotedSubgraphRead(res) {
|
|
5186
5236
|
if (!res.isError)
|
|
5187
5237
|
return false;
|
|
5188
|
-
return /^Error:\s*Node\s+\S+(?:\s+\([^)]*\))?\s+is not a subgraph\b/i.test(textOfToolResult(res));
|
|
5238
|
+
return /^Error:\s*Node\s+\S+(?:\s+\((?:(?:[^()]|\([^()]*\))*|[^)]*)\))?\s+is not a subgraph\b/i.test(textOfToolResult(res));
|
|
5189
5239
|
}
|
|
5190
5240
|
/**
|
|
5191
5241
|
* panel#1869 — the panel's `[canvas-root-divergence]` refusal is a DIAGNOSIS,
|
|
@@ -5564,6 +5614,41 @@ function promotedPanelBuildRefusal(ctx, nodeId, widget, reason, capability) {
|
|
|
5564
5614
|
* indeterminate read fails closed because the panel can resolve a promoted
|
|
5565
5615
|
* container to its unsafe inner node.
|
|
5566
5616
|
*/
|
|
5617
|
+
/**
|
|
5618
|
+
* Why the panel binding is no longer the one we captured, or undefined when it is.
|
|
5619
|
+
*
|
|
5620
|
+
* WHY THIS IS A FUNCTION AND NOT A THIRD COPY. An early `return null` out of the
|
|
5621
|
+
* promoted pre-dispatch check does not mean "refuse" — it means "this is not a promoted
|
|
5622
|
+
* write, proceed on the ordinary path". So every exit that crosses an `await` is a WRITE
|
|
5623
|
+
* AUTHORIZATION issued against a binding that may have moved underneath it, and each one
|
|
5624
|
+
* needs the same re-read. There were two hand-written copies of these five checks; a third
|
|
5625
|
+
* exit was then added between them without one, which is #2401, and fixing that exit alone
|
|
5626
|
+
* left the next one bare, which is #2409. One copy, called by each exit, so the next early
|
|
5627
|
+
* return has one obvious thing to call.
|
|
5628
|
+
*
|
|
5629
|
+
* `where` is appended to each message so the caller keeps naming its own await.
|
|
5630
|
+
*/
|
|
5631
|
+
function panelBindingDriftReason(ctx, where, hasIdentityApi, identityBefore, tabBefore) {
|
|
5632
|
+
if (!hasIdentityApi)
|
|
5633
|
+
return `the receiver identity was unavailable ${where}`;
|
|
5634
|
+
if (!isUsablePanelConnectionIdentity(identityBefore)) {
|
|
5635
|
+
return `the panel connection identity was unavailable ${where}`;
|
|
5636
|
+
}
|
|
5637
|
+
let identityAfter;
|
|
5638
|
+
try {
|
|
5639
|
+
identityAfter = ctx.panelConnectionIdentity?.();
|
|
5640
|
+
}
|
|
5641
|
+
catch {
|
|
5642
|
+
return `the panel connection identity became unreadable ${where}`;
|
|
5643
|
+
}
|
|
5644
|
+
if (ctx.tabId !== tabBefore || !isUsablePanelConnectionIdentity(identityAfter)) {
|
|
5645
|
+
return `the panel tab or connection changed ${where}`;
|
|
5646
|
+
}
|
|
5647
|
+
if (!samePanelConnectionIdentity(identityBefore, identityAfter)) {
|
|
5648
|
+
return `the panel session or connection changed ${where}`;
|
|
5649
|
+
}
|
|
5650
|
+
return undefined;
|
|
5651
|
+
}
|
|
5567
5652
|
async function preparePromotedWidgetWrite(ctx, nodeId, widget) {
|
|
5568
5653
|
// Production PanelToolCtx is always backed by UiBridge, which exposes this
|
|
5569
5654
|
// per-hello capability getter. A few transport-only forwarding contexts (and
|
|
@@ -5600,12 +5685,23 @@ async function preparePromotedWidgetWrite(ctx, nodeId, widget) {
|
|
|
5600
5685
|
// absent. An unreadable scope probe is not permission for an outer write;
|
|
5601
5686
|
// it falls through to the existing conservative graph_get_subgraph path.
|
|
5602
5687
|
const targetScope = await readPromotedTargetScope(ctx, nodeId);
|
|
5603
|
-
if (targetScope?.activeView === "root" && targetScope.node === "ordinary")
|
|
5688
|
+
if (targetScope?.activeView === "root" && targetScope.node === "ordinary") {
|
|
5689
|
+
// The scope probe crossed an await. Re-read the binding before taking the
|
|
5690
|
+
// ordinary fast path; otherwise a tab/connection rebind can make this
|
|
5691
|
+
// probe's ordinary classification authorize a write on the new receiver.
|
|
5692
|
+
const drift = panelBindingDriftReason(ctx, "after the scope probe", hasIdentityApi, identityBefore, tabBefore);
|
|
5693
|
+
if (drift)
|
|
5694
|
+
return promotedWriteRefusal(widget, drift);
|
|
5604
5695
|
return null;
|
|
5696
|
+
}
|
|
5605
5697
|
const sub = await ctx.call({ cmd: "graph_get_subgraph", node_id: nodeId });
|
|
5606
5698
|
if (sub.isError) {
|
|
5607
|
-
if (isDefinitiveNonPromotedSubgraphRead(sub))
|
|
5699
|
+
if (isDefinitiveNonPromotedSubgraphRead(sub)) {
|
|
5700
|
+
const drift2 = panelBindingDriftReason(ctx, "after the subgraph read", hasIdentityApi, identityBefore, tabBefore);
|
|
5701
|
+
if (drift2)
|
|
5702
|
+
return promotedWriteRefusal(widget, drift2);
|
|
5608
5703
|
return null;
|
|
5704
|
+
}
|
|
5609
5705
|
return promotedSubgraphReadRefusal(widget, sub, "graph_get_subgraph could not determine whether the addressed node is a promoted container");
|
|
5610
5706
|
}
|
|
5611
5707
|
let payload = parseToolResultJson(sub);
|
|
@@ -8522,6 +8618,7 @@ async function rebindWorkflowFence(ctx, opts) {
|
|
|
8522
8618
|
before,
|
|
8523
8619
|
kind: "no_uuid",
|
|
8524
8620
|
why: "the active workflow record carries no usable workflow_uuid",
|
|
8621
|
+
active,
|
|
8525
8622
|
};
|
|
8526
8623
|
}
|
|
8527
8624
|
// "already current" requires a KNOWN prior fence equal to the live uuid. An
|
|
@@ -9161,8 +9258,19 @@ WHAT TO DO: settle it with a graph read — call panel_graph_outline. That ` +
|
|
|
9161
9258
|
* active object and accept the returned UUID only while it still names this
|
|
9162
9259
|
* open's canonical target; otherwise leave the old stamp to fail closed.
|
|
9163
9260
|
*/
|
|
9164
|
-
async function refreshOpenWorkflowUuid(ctx, requestedPath, openResult) {
|
|
9261
|
+
async function refreshOpenWorkflowUuid(ctx, requestedPath, openResult, legacyRebind) {
|
|
9165
9262
|
const parsedOpen = parseToolResultJson(openResult);
|
|
9263
|
+
// #971 compatibility: older/lightweight bridges may return only an explicit
|
|
9264
|
+
// transport route for a successful open. The proof is supplied by the one
|
|
9265
|
+
// immediately following open only after the caller consumed it before any
|
|
9266
|
+
// await; it is never a substitute for the modern bare-alias `opened.path` +
|
|
9267
|
+
// confirmed active workflow contract.
|
|
9268
|
+
const legacyReboundRoute = legacyRebind?.tabId === ctx.tabId &&
|
|
9269
|
+
legacyRebind.savedIdentity === canonicalBareSavedIdentity(requestedPath) &&
|
|
9270
|
+
!openResult.isError &&
|
|
9271
|
+
parsedOpen?.ok === true &&
|
|
9272
|
+
parsedOpen.routedTo === ctx.tabId &&
|
|
9273
|
+
(typeof ctx.bridge.canReach !== "function" || ctx.bridge.canReach(ctx.tabId));
|
|
9166
9274
|
const opened = parsedOpen?.opened;
|
|
9167
9275
|
const openedPath = opened && typeof opened === "object" && typeof opened.path === "string"
|
|
9168
9276
|
? opened.path
|
|
@@ -9171,11 +9279,118 @@ async function refreshOpenWorkflowUuid(ctx, requestedPath, openResult) {
|
|
|
9171
9279
|
// alias/basename to a path, but that reply must never retroactively turn the
|
|
9172
9280
|
// alias into a UUID-refresh authorization. Require the reply to corroborate
|
|
9173
9281
|
// the original exact saved identity before consulting the live active record.
|
|
9282
|
+
const requestedSavedPath = canonicalSavedWorkflowPath(requestedPath);
|
|
9283
|
+
const requestedIsBareAlias = !!requestedSavedPath && !requestedSavedPath.includes("/");
|
|
9174
9284
|
const requestedIdentity = canonicalRequestedSavedIdentity(requestedPath);
|
|
9175
9285
|
const openedIdentity = openedPath
|
|
9176
9286
|
? canonicalSavedRecordIdentity({ path: openedPath, routing_key: parsedOpen?.routing_key })
|
|
9177
9287
|
: null;
|
|
9288
|
+
if (requestedIsBareAlias) {
|
|
9289
|
+
// A bare filename is only a selector. It is not safe to let a native
|
|
9290
|
+
// success through unless the panel both resolved it to a saved path and
|
|
9291
|
+
// gave us a fresh, explicitly confirmed active observation. In particular,
|
|
9292
|
+
// an omitted `opened.path` is not a resolution, and an omitted
|
|
9293
|
+
// `active_confirmed` is not confirmation (#1639).
|
|
9294
|
+
if (!openedPath) {
|
|
9295
|
+
if (legacyReboundRoute)
|
|
9296
|
+
return null;
|
|
9297
|
+
return {
|
|
9298
|
+
drifted: true,
|
|
9299
|
+
unverified: true,
|
|
9300
|
+
activeLabel: "the panel did not return a resolved path for this filename",
|
|
9301
|
+
};
|
|
9302
|
+
}
|
|
9303
|
+
if (!canonicalRequestedSavedIdentity(openedPath)) {
|
|
9304
|
+
return {
|
|
9305
|
+
drifted: true,
|
|
9306
|
+
unverified: true,
|
|
9307
|
+
activeLabel: "the panel did not return a resolved path for this filename",
|
|
9308
|
+
};
|
|
9309
|
+
}
|
|
9310
|
+
let list = null;
|
|
9311
|
+
try {
|
|
9312
|
+
const res = await ctx.call({ cmd: "workflow_list" }, 6000);
|
|
9313
|
+
if (!res?.isError)
|
|
9314
|
+
list = parseToolResultJson(res);
|
|
9315
|
+
}
|
|
9316
|
+
catch {
|
|
9317
|
+
list = null;
|
|
9318
|
+
}
|
|
9319
|
+
if (!list || list.active_confirmed !== true) {
|
|
9320
|
+
return {
|
|
9321
|
+
drifted: true,
|
|
9322
|
+
unverified: true,
|
|
9323
|
+
activeLabel: "the panel did not return a freshly confirmed active workflow after resolving this filename",
|
|
9324
|
+
};
|
|
9325
|
+
}
|
|
9326
|
+
const resolvedIdentity = canonicalSavedRecordIdentity({
|
|
9327
|
+
path: openedPath,
|
|
9328
|
+
routing_key: parsedOpen?.routing_key,
|
|
9329
|
+
});
|
|
9330
|
+
const activeIdentity = canonicalSavedRecordIdentity(list.active);
|
|
9331
|
+
if (resolvedIdentity && activeIdentity === resolvedIdentity) {
|
|
9332
|
+
// The re-read proves which resolved path is active, but the caller only
|
|
9333
|
+
// supplied an alias. Preserve #716's no-adoption rule.
|
|
9334
|
+
return null;
|
|
9335
|
+
}
|
|
9336
|
+
if (activeIdentity) {
|
|
9337
|
+
return { drifted: true, activeLabel: describeActiveRecord(list.active) };
|
|
9338
|
+
}
|
|
9339
|
+
return {
|
|
9340
|
+
drifted: true,
|
|
9341
|
+
unverified: true,
|
|
9342
|
+
activeLabel: "the panel returned an unconfirmed active workflow after resolving this filename",
|
|
9343
|
+
};
|
|
9344
|
+
}
|
|
9178
9345
|
if (!requestedIdentity || requestedIdentity !== openedIdentity) {
|
|
9346
|
+
// A bare filename is an alias, not a saved identity. The panel's `opened.path`
|
|
9347
|
+
// is the resolution of that alias; once it is available, corroborate THAT exact
|
|
9348
|
+
// path against a fresh active-list read before allowing the caller to treat the
|
|
9349
|
+
// success as an active-canvas success. Without this branch, the alias exits here
|
|
9350
|
+
// before any live observation and a stale previous tab can be reported as active
|
|
9351
|
+
// and bound (#1639).
|
|
9352
|
+
const resolvedIdentity = openedPath ? canonicalRequestedSavedIdentity(openedPath) : null;
|
|
9353
|
+
if (!requestedIdentity && resolvedIdentity) {
|
|
9354
|
+
let list = null;
|
|
9355
|
+
try {
|
|
9356
|
+
const res = await ctx.call({ cmd: "workflow_list" }, 6000);
|
|
9357
|
+
if (!res?.isError)
|
|
9358
|
+
list = parseToolResultJson(res);
|
|
9359
|
+
}
|
|
9360
|
+
catch {
|
|
9361
|
+
list = null;
|
|
9362
|
+
}
|
|
9363
|
+
if (!list) {
|
|
9364
|
+
return {
|
|
9365
|
+
drifted: true,
|
|
9366
|
+
unverified: true,
|
|
9367
|
+
activeLabel: "the panel did not return a confirmed active workflow after resolving this filename",
|
|
9368
|
+
};
|
|
9369
|
+
}
|
|
9370
|
+
if (list.active_confirmed !== true) {
|
|
9371
|
+
return {
|
|
9372
|
+
drifted: true,
|
|
9373
|
+
unverified: true,
|
|
9374
|
+
activeLabel: "the panel returned an unconfirmed active workflow after resolving this filename",
|
|
9375
|
+
};
|
|
9376
|
+
}
|
|
9377
|
+
const activeIdentity = canonicalSavedRecordIdentity(list.active);
|
|
9378
|
+
if (activeIdentity === resolvedIdentity) {
|
|
9379
|
+
// The re-read proves which resolved path is active, but the caller only
|
|
9380
|
+
// supplied an alias. Preserve #716's no-adoption rule: an alias may be
|
|
9381
|
+
// observed for the #1639 wrong-canvas guard, never promoted into a new
|
|
9382
|
+
// command-fence UUID by the reply-resolved path.
|
|
9383
|
+
return null;
|
|
9384
|
+
}
|
|
9385
|
+
if (activeIdentity) {
|
|
9386
|
+
return { drifted: true, activeLabel: describeActiveRecord(list.active) };
|
|
9387
|
+
}
|
|
9388
|
+
return {
|
|
9389
|
+
drifted: true,
|
|
9390
|
+
unverified: true,
|
|
9391
|
+
activeLabel: "the panel returned an unconfirmed active workflow after resolving this filename",
|
|
9392
|
+
};
|
|
9393
|
+
}
|
|
9179
9394
|
// #812 — the SAVED corroboration above can never succeed for an unsaved
|
|
9180
9395
|
// target (there is no path), so try the parallel UNSAVED identity: the
|
|
9181
9396
|
// caller's literal token against the panel's own proven routing_key for
|
|
@@ -9413,6 +9628,11 @@ function noReachableTabFail(cmd, ctx) {
|
|
|
9413
9628
|
`sent. Retry in a moment, or rebind with panel_set_workflow_target({mode:"current"}).`);
|
|
9414
9629
|
}
|
|
9415
9630
|
async function openWorkflowWithVerify(path, ctx) {
|
|
9631
|
+
// #971 — consume the explicit-current proof BEFORE the first await. This
|
|
9632
|
+
// makes it one-shot even when this open fails, and prevents a concurrent or
|
|
9633
|
+
// later open from inheriting a proof belonging to an earlier operation.
|
|
9634
|
+
const legacyRebind = ctx.lastExplicitCurrentRebind;
|
|
9635
|
+
ctx.lastExplicitCurrentRebind = undefined;
|
|
9416
9636
|
// #402: after a full ComfyUI restart the browser tab re-registers a few seconds
|
|
9417
9637
|
// later. Awaiting a stable binding BEFORE dispatching a mutating workflow_open
|
|
9418
9638
|
// (nothing is sent yet — no double-apply risk) means the command reaches a live
|
|
@@ -9476,7 +9696,7 @@ async function openWorkflowWithVerify(path, ctx) {
|
|
|
9476
9696
|
`save-as, or edit expecting ${path}. Call panel_list_workflows to see the current state.`);
|
|
9477
9697
|
}
|
|
9478
9698
|
{
|
|
9479
|
-
const drift = await refreshOpenWorkflowUuid(ctx, path, res);
|
|
9699
|
+
const drift = await refreshOpenWorkflowUuid(ctx, path, res, legacyRebind);
|
|
9480
9700
|
// #887 — the read above is the ONLY observation in this whole path taken
|
|
9481
9701
|
// after a real round trip, so it is the only thing that can catch the active
|
|
9482
9702
|
// pointer having settled elsewhere. It already declined to adopt the uuid;
|
|
@@ -9489,6 +9709,12 @@ async function openWorkflowWithVerify(path, ctx) {
|
|
|
9489
9709
|
// act is typically a write. The session's fence is untouched (nothing was
|
|
9490
9710
|
// adopted), so the tab that IS active keeps its own protection.
|
|
9491
9711
|
if (drift) {
|
|
9712
|
+
if (drift.unverified) {
|
|
9713
|
+
return fail(`workflow_open: ${path} was reported applied, but the panel could not prove that ` +
|
|
9714
|
+
`${drift.activeLabel}. The active canvas is UNKNOWN, so this open is not a ` +
|
|
9715
|
+
`success and this session's workflow identity was NOT re-pointed. Inspect ` +
|
|
9716
|
+
`panel_list_workflows before reading or writing the graph.`);
|
|
9717
|
+
}
|
|
9492
9718
|
return fail(`workflow_open: ${path} was opened, but ${drift.activeLabel} is the ACTIVE workflow now — ` +
|
|
9493
9719
|
`the panel confirmed this on a re-read after the open completed. This session's ` +
|
|
9494
9720
|
`workflow identity was NOT re-pointed at ${path}, so the canvas you would read or ` +
|
|
@@ -9829,6 +10055,11 @@ function canonicalRequestedSavedIdentity(path) {
|
|
|
9829
10055
|
// authorize replacing its existing UUID stamp.
|
|
9830
10056
|
return canonicalPath && canonicalPath.includes("/") ? `wf:${canonicalPath}` : null;
|
|
9831
10057
|
}
|
|
10058
|
+
/** Canonical identity for a bare saved-workflow selector, for one-shot #971 proof matching. */
|
|
10059
|
+
function canonicalBareSavedIdentity(path) {
|
|
10060
|
+
const canonicalPath = canonicalSavedWorkflowPath(path);
|
|
10061
|
+
return canonicalPath && !canonicalPath.includes("/") ? `wf:${canonicalPath}` : null;
|
|
10062
|
+
}
|
|
9832
10063
|
/**
|
|
9833
10064
|
* Canonical `wf:<path>` identity, but only for a complete corroborating record.
|
|
9834
10065
|
* Command-fence replacement is stricter than pin routing: a same-path record
|
|
@@ -14918,8 +15149,9 @@ export function buildPanelToolDefs() {
|
|
|
14918
15149
|
// inner mapping and set it there (the issue's own workaround), then
|
|
14919
15150
|
// leave the subgraph so the caller's scope is unchanged.
|
|
14920
15151
|
const refusal = parseContradictoryPromotedWidgetRefusal(textOfToolResult(first), args.widget);
|
|
14921
|
-
if (!refusal || String(refusal.nodeId) !== String(args.node_id))
|
|
14922
|
-
return first;
|
|
15152
|
+
if (!refusal || String(refusal.nodeId) !== String(args.node_id)) {
|
|
15153
|
+
return withModelInventoryDisclosure(first, args.widget, value, ctx);
|
|
15154
|
+
}
|
|
14923
15155
|
if (refusal.widget !== args.widget) {
|
|
14924
15156
|
const remapped = await guardedWrite(args.node_id, refusal.widget);
|
|
14925
15157
|
if (!remapped.isError) {
|
|
@@ -17087,6 +17319,10 @@ export function buildPanelToolDefs() {
|
|
|
17087
17319
|
if (mode === "pinned" && !(path ?? "").trim()) {
|
|
17088
17320
|
return fail("Provide path when pinning — use panel_list_workflows to list open workflows.");
|
|
17089
17321
|
}
|
|
17322
|
+
// A prior explicit recovery proof must not survive another targeting
|
|
17323
|
+
// call. It is valid only for the open immediately following the rebind
|
|
17324
|
+
// that produced it.
|
|
17325
|
+
ctx.lastExplicitCurrentRebind = undefined;
|
|
17090
17326
|
// mode:'current' is the explicit, user/agent-initiated "rebind me to the
|
|
17091
17327
|
// tab that's live now" consent signal. Self-heal a session whose captured
|
|
17092
17328
|
// tab id was orphaned (reconnect/reload/workflow-switch) BEFORE writing the
|
|
@@ -17099,8 +17335,10 @@ export function buildPanelToolDefs() {
|
|
|
17099
17335
|
// on a successful turn-pin recovery. Track that separately from the
|
|
17100
17336
|
// real-tab rebind note below.
|
|
17101
17337
|
let currentModeTurnRepinned = false;
|
|
17338
|
+
let explicitCurrentRebindBefore;
|
|
17102
17339
|
if (mode === "current" && ctx.rebindToActiveTab) {
|
|
17103
17340
|
const before = ctx.tabId;
|
|
17341
|
+
explicitCurrentRebindBefore = before;
|
|
17104
17342
|
const recoveringScope = isScopeAddress(before);
|
|
17105
17343
|
// Hold the send() wait BEFORE the first await so a same-batch sibling
|
|
17106
17344
|
// that already hit the null pin waits instead of minting #884.
|
|
@@ -17360,6 +17598,21 @@ export function buildPanelToolDefs() {
|
|
|
17360
17598
|
rebindNote += ` The panel dropped mid-call: this session moved from tab ${tabBeforeProbe} onto tab ${ctx.tabId}, and mode:"current" was applied there too.`;
|
|
17361
17599
|
}
|
|
17362
17600
|
}
|
|
17601
|
+
// #971 — preserve the old-panel recovery only when the current-mode
|
|
17602
|
+
// operation itself corroborated the saved identity now selected. The
|
|
17603
|
+
// proof is consumed by the next open before its first await, so a failed
|
|
17604
|
+
// open, a different alias, or a later open cannot inherit it.
|
|
17605
|
+
if (mode === "current" &&
|
|
17606
|
+
!deferredBind &&
|
|
17607
|
+
explicitCurrentRebindBefore !== undefined &&
|
|
17608
|
+
ctx.tabId !== explicitCurrentRebindBefore &&
|
|
17609
|
+
fenceRebind &&
|
|
17610
|
+
"active" in fenceRebind) {
|
|
17611
|
+
const savedIdentity = canonicalSavedRecordIdentity(fenceRebind.active);
|
|
17612
|
+
if (savedIdentity) {
|
|
17613
|
+
ctx.lastExplicitCurrentRebind = { tabId: ctx.tabId, savedIdentity };
|
|
17614
|
+
}
|
|
17615
|
+
}
|
|
17363
17616
|
// panel#1529 — "Graph tools will target that workflow" is a CLAIM ABOUT
|
|
17364
17617
|
// ROUTING, and this call only ever wrote the workflow-target store. It is
|
|
17365
17618
|
// true when the panel confirmed the target IS the live canvas, because a
|
|
@@ -19535,7 +19788,7 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
|
|
|
19535
19788
|
// and refuse to report freed:true when a device is still pinned.
|
|
19536
19789
|
return annotateFreeVramAck(ctx, res);
|
|
19537
19790
|
}),
|
|
19538
|
-
def("panel_show_media", "Display one or more images, videos or AUDIO files directly in the panel chat. Use this whenever the user asks to SEE, SHOW, PLAY or HEAR a file — a disk path you composited/downloaded/generated (absolute path on the orchestrator host) OR a ComfyUI output ref ({ filename, subfolder?, type? }). Items are rendered as media cards in the agent chat area — audio gets a real player (.wav/.mp3/.flac/.ogg/.oga/.opus/.m4a/.aac), so a generated TTS or voice take is played back with this tool, not described; supply optional captions. You are never sent the audio yourself and cannot hear it. Max 8 items per call. A path item OVER the 20 MB inline cap that is not under any directory ComfyUI serves can still be shown by passing stage:true on that item — the orchestrator COPIES it into <output>/_panel_staged (an opt-in, persistent disk write; 512 MB per-file and 2 GB total caps) and displays the copy by reference. NEVER describe an image with emoji or text placeholders — call this tool instead.", {
|
|
19791
|
+
def("panel_show_media", "Display one or more images, videos or AUDIO files directly in the panel chat. Use this whenever the user asks to SEE, SHOW, PLAY or HEAR a file — a disk path you composited/downloaded/generated (absolute path on the orchestrator host) OR a ComfyUI output ref ({ filename, subfolder?, type? }). Items are rendered as media cards in the agent chat area — audio gets a real player (.wav/.mp3/.flac/.ogg/.oga/.opus/.m4a/.aac), so a generated TTS or voice take is played back with this tool, not described; supply optional captions. You are never sent the audio yourself and cannot hear it. Max 8 items per call. A path item OVER the 20 MB inline cap that is not under any directory ComfyUI serves can still be shown by passing stage:true on that item — the orchestrator COPIES it into <output>/_panel_staged (an opt-in, persistent disk write; 512 MB per-file and 2 GB total caps) and displays the copy by reference. Staging needs a LOCAL ComfyUI: it is a filesystem copy into ComfyUI's own output directory, so it cannot run against a remote target (--comfyui-url on another host) and the call reports that instead of staging. NEVER describe an image with emoji or text placeholders — call this tool instead.", {
|
|
19539
19792
|
items: z
|
|
19540
19793
|
.array(z.object({
|
|
19541
19794
|
// #1968 — OPTIONAL in the shape, required by normalizeShowMediaItem
|