comfyui-mcp 0.49.7 → 0.49.8
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/README.md +0 -37
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -1
- package/dist/orchestrator/index.js +69 -1
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/orchestrator/ollama-backend.js +71 -3
- package/dist/orchestrator/ollama-backend.js.map +1 -1
- package/dist/orchestrator/panel-tools.js +349 -57
- package/dist/orchestrator/panel-tools.js.map +1 -1
- package/dist/services/code-provider-auth.js +4 -1
- package/dist/services/code-provider-auth.js.map +1 -1
- package/dist/services/panel-pin-guard.js +106 -14
- package/dist/services/panel-pin-guard.js.map +1 -1
- package/dist/services/panel-secrets.js +21 -8
- package/dist/services/panel-secrets.js.map +1 -1
- package/dist/services/ui-bridge.js +46 -3
- package/dist/services/ui-bridge.js.map +1 -1
- package/dist/services/userdata-library.js +150 -0
- package/dist/services/userdata-library.js.map +1 -0
- package/dist/tools/compact.js +106 -0
- package/dist/tools/compact.js.map +1 -1
- package/dist/tools/workflow-library.js +107 -11
- package/dist/tools/workflow-library.js.map +1 -1
- package/dist/transport/cli.js +61 -0
- package/dist/transport/cli.js.map +1 -1
- package/package.json +1 -1
- package/scripts/tool-doc-examples.ts +5 -3
|
@@ -36,11 +36,12 @@ import { withWorkflowTarget, } from "../services/workflow-target-store.js";
|
|
|
36
36
|
import { addUserMcpServer, readUserMcpServers, removeUserMcpServer, setUserMcpServerSecret, } from "../services/user-mcp-config.js";
|
|
37
37
|
import { setComfyuiSecret, setAgentSecret, isAllowedAgentSecretKey } from "../services/panel-secrets.js";
|
|
38
38
|
import { flattenUiWorkflow } from "../services/flatten-workflow.js";
|
|
39
|
+
import { listWorkflowLibraryKeys, userdataFetch } from "../services/userdata-library.js";
|
|
39
40
|
import { getNsfwConsent, setNsfwConsent } from "../services/panel-settings.js";
|
|
40
41
|
import { QueueMonitor } from "../services/queue-monitor.js";
|
|
41
42
|
import { RunCompletions } from "./run-completion-journal.js";
|
|
42
43
|
import { AskAnswers, askFingerprint, PANEL_ASK_ID_PREFIX, } from "./ask-answer-journal.js";
|
|
43
|
-
import {
|
|
44
|
+
import { getObjectInfo, backfillObjectInfo, resetClient, resetObjectInfoCache, } from "../comfyui/client.js";
|
|
44
45
|
import { convertUiToApi, collectNodeTypes } from "../services/workflow-converter.js";
|
|
45
46
|
import { restartComfyUI, preflightLocalRestart, recordRestartDispatch, clearRestartDispatch, getRestartDispatchRecord, RESTART_DISPATCH_CAUSATION_WINDOW_MS, PROCESS_WIDE_RESTART_DISPATCH_TOKEN, __processControlTestHooks, } from "../services/process-control.js";
|
|
46
47
|
import { resetManagerApiCache } from "../services/manager-api-cache.js";
|
|
@@ -1087,6 +1088,331 @@ function fixedCapHint(what, shown, total, targeted) {
|
|
|
1087
1088
|
return (`Showing ${shownOf(shown, total)} ${what} — this view has a FIXED cap and no parameter raises it. ` +
|
|
1088
1089
|
targeted);
|
|
1089
1090
|
}
|
|
1091
|
+
// ---- #807: ONE budget, for the WHOLE panel_query_graph reply ----------------------
|
|
1092
|
+
//
|
|
1093
|
+
// `max_chars` used to bound the `text` field and nothing else. Everything else in the
|
|
1094
|
+
// reply — the `groups` array with each group's full member `node_ids`, and the subgraph
|
|
1095
|
+
// `rails` — rode ALONGSIDE it under separate fixed caps, and was serialized BEFORE the
|
|
1096
|
+
// rows the caller had asked for. On a 690-node graph that is not a rounding error: at
|
|
1097
|
+
// the panel's own caps those riders alone can render to well over 100k characters, so a
|
|
1098
|
+
// reply announcing "truncated at 12 of 690 by max_chars=12000" could hand back ten times
|
|
1099
|
+
// that. A budget figure that does not describe the actual payload is a fabricated
|
|
1100
|
+
// observation, and the agent that reads it concludes the TOOL cannot show it node
|
|
1101
|
+
// detail — which is exactly what the reporter's session concluded.
|
|
1102
|
+
//
|
|
1103
|
+
// The accounting is applied HERE, once, over the finished payload, for the same reason
|
|
1104
|
+
// the #809 riders are: this is the last place the reply is touched before the caller
|
|
1105
|
+
// sees it, it measures the EXACT string that will be returned (pretty-printed JSON,
|
|
1106
|
+
// escaping and all), and it holds against every panel build rather than only the ones
|
|
1107
|
+
// that ship after it.
|
|
1108
|
+
//
|
|
1109
|
+
// Two rules govern what gets shed:
|
|
1110
|
+
// 1. The rows that ANSWER THE QUERY are never dropped to make room for context. The
|
|
1111
|
+
// riders are spent from what is left over, and are moved to the END of the object
|
|
1112
|
+
// so the answer is also what a reader meets first.
|
|
1113
|
+
// 2. Coverage before resolution, mirroring panel_graph_outline's ladder: dropping
|
|
1114
|
+
// every group's membership still leaves a complete group index, whereas listing
|
|
1115
|
+
// half the groups reads as "this graph has that many groups".
|
|
1116
|
+
const QUERY_GRAPH_MAX_CHARS_FLOOR = 500;
|
|
1117
|
+
const QUERY_GRAPH_MAX_CHARS_DEFAULT = 12000;
|
|
1118
|
+
const QUERY_GRAPH_MAX_CHARS_CEILING = 60000;
|
|
1119
|
+
/** The panel's own clamp for graph_query's budget, mirrored so the figure this module
|
|
1120
|
+
* reports and enforces is the one the panel was actually working to. */
|
|
1121
|
+
function clampQueryGraphMaxChars(v) {
|
|
1122
|
+
const n = Number(v);
|
|
1123
|
+
if (!Number.isFinite(n))
|
|
1124
|
+
return QUERY_GRAPH_MAX_CHARS_DEFAULT;
|
|
1125
|
+
return Math.min(Math.max(Math.floor(n), QUERY_GRAPH_MAX_CHARS_FLOOR), QUERY_GRAPH_MAX_CHARS_CEILING);
|
|
1126
|
+
}
|
|
1127
|
+
/** The reply fields that are CONTEXT rather than answer. `viewing` is deliberately not
|
|
1128
|
+
* one of them: it identifies which graph the answer describes, it is a couple of small
|
|
1129
|
+
* fields, and a reply that cannot say what it is about is worse than a bigger one. */
|
|
1130
|
+
const QUERY_GRAPH_RIDER_KEYS = [
|
|
1131
|
+
"groups_truncated",
|
|
1132
|
+
"groups_truncation_hint",
|
|
1133
|
+
"groups",
|
|
1134
|
+
"rails",
|
|
1135
|
+
];
|
|
1136
|
+
/** The fields THIS accounting authors. Any inbound copy is dropped before measuring, so
|
|
1137
|
+
* a stale claim from a different measurement can never ride out as if this one made it
|
|
1138
|
+
* (independent gate P0). `max_chars` is re-set unconditionally just below. */
|
|
1139
|
+
const QUERY_GRAPH_OWNED_KEYS = new Set([
|
|
1140
|
+
"max_chars",
|
|
1141
|
+
"groups_membership_omitted",
|
|
1142
|
+
"groups_omitted",
|
|
1143
|
+
"rails_omitted",
|
|
1144
|
+
"budget_overrun",
|
|
1145
|
+
]);
|
|
1146
|
+
/** A group reduced to its INDEX form: which groups exist, what they are called, and how
|
|
1147
|
+
* many nodes each holds. Membership and geometry are what grow with the graph. */
|
|
1148
|
+
function groupIndexEntry(g) {
|
|
1149
|
+
if (!g || typeof g !== "object" || Array.isArray(g))
|
|
1150
|
+
return g;
|
|
1151
|
+
const r = g;
|
|
1152
|
+
const out = {};
|
|
1153
|
+
if ("id" in r)
|
|
1154
|
+
out.id = r.id;
|
|
1155
|
+
if ("title" in r)
|
|
1156
|
+
out.title = r.title;
|
|
1157
|
+
// `node_count` is the group's TRUE size on every panel that reports it. Falling back
|
|
1158
|
+
// to the length of `node_ids` is only sound when that list was not itself clipped —
|
|
1159
|
+
// otherwise the count would understate the group, which is the same lie one rung down.
|
|
1160
|
+
if (typeof r.node_count === "number")
|
|
1161
|
+
out.node_count = r.node_count;
|
|
1162
|
+
else if (Array.isArray(r.node_ids) && r.node_ids_truncated === undefined)
|
|
1163
|
+
out.node_count = r.node_ids.length;
|
|
1164
|
+
return out;
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Fit the WHOLE panel_query_graph reply inside the `max_chars` it advertises, by
|
|
1168
|
+
* shedding CONTEXT — never the rows that answer the query.
|
|
1169
|
+
*
|
|
1170
|
+
* EVERY reply is measured, including the ones with no riders to shed (codex gate
|
|
1171
|
+
* SEVERE): the early "there is nothing to cut, so skip the accounting" shortcut let a
|
|
1172
|
+
* reply with a large `viewing`, a field this build has never seen, or simply very long
|
|
1173
|
+
* rows exceed the budget it reports with nothing said. There is no third outcome here —
|
|
1174
|
+
* a reply either fits the budget it names or carries `budget_overrun` saying it does
|
|
1175
|
+
* not. Nothing is invented on the way: a result that fits is never dressed up as a
|
|
1176
|
+
* truncated one (the false-truncation defect #809 also exists to remove).
|
|
1177
|
+
*
|
|
1178
|
+
* An error or non-JSON reply is passed through untouched. It makes no budget CLAIM —
|
|
1179
|
+
* there is no `max_chars` on it to be wrong — and rewriting a failure message to
|
|
1180
|
+
* mention a character budget would bury the failure.
|
|
1181
|
+
*
|
|
1182
|
+
* EVERY text block is counted, not only the one carrying the JSON, and a non-text block
|
|
1183
|
+
* (this tool emits none, but the shape allows them) has its PRESENCE disclosed rather
|
|
1184
|
+
* than silently ignored — `max_chars` bounds characters, and calling image bytes
|
|
1185
|
+
* characters would be its own false figure.
|
|
1186
|
+
*/
|
|
1187
|
+
function fitQueryGraphReply(res, requested) {
|
|
1188
|
+
const payload = parseToolResultJson(res);
|
|
1189
|
+
if (!payload)
|
|
1190
|
+
return res;
|
|
1191
|
+
const idx = res.content.findIndex((c) => c.type === "text");
|
|
1192
|
+
if (idx < 0)
|
|
1193
|
+
return res;
|
|
1194
|
+
const budget = clampQueryGraphMaxChars(requested);
|
|
1195
|
+
// EVERY text block counts, not just the one carrying the JSON (independent gate P0).
|
|
1196
|
+
// Measuring the first block and leaving the rest untouched is the SAME defect this
|
|
1197
|
+
// function exists to remove — a budget describing one part of a reply while something
|
|
1198
|
+
// rides beside it — relocated from `groups`/`rails` to the block list. A small fitted
|
|
1199
|
+
// payload next to an unmeasured 100k second block would report `max_chars` and no
|
|
1200
|
+
// overrun. "The last place the reply is touched" has to mean the whole reply.
|
|
1201
|
+
const siblingText = res.content.reduce((n, c, i) => (i !== idx && c.type === "text" ? n + (c.text?.length ?? 0) : n), 0);
|
|
1202
|
+
// Blocks that are not text (this tool emits none, but the shape allows them) are not
|
|
1203
|
+
// counted — `max_chars` bounds characters, and calling image bytes characters would be
|
|
1204
|
+
// its own false figure — so their PRESENCE is disclosed instead of silently ignored.
|
|
1205
|
+
const nonTextBlocks = res.content.filter((c) => c.type !== "text").length;
|
|
1206
|
+
const render = (p) => JSON.stringify(p, null, 2);
|
|
1207
|
+
/** The whole reply's character count: the fitted JSON plus every sibling text block. */
|
|
1208
|
+
const sizeOf = (p) => render(p).length + siblingText;
|
|
1209
|
+
const replaceWith = (p) => ({
|
|
1210
|
+
...res,
|
|
1211
|
+
content: res.content.map((c, i) => i === idx && c.type === "text" ? { ...c, text: render(p) } : c),
|
|
1212
|
+
});
|
|
1213
|
+
const riderKeys = new Set(QUERY_GRAPH_RIDER_KEYS);
|
|
1214
|
+
const answer = {};
|
|
1215
|
+
const riders = {};
|
|
1216
|
+
for (const [k, v] of Object.entries(payload)) {
|
|
1217
|
+
if (riderKeys.has(k))
|
|
1218
|
+
riders[k] = v;
|
|
1219
|
+
else if (!QUERY_GRAPH_OWNED_KEYS.has(k))
|
|
1220
|
+
answer[k] = v;
|
|
1221
|
+
}
|
|
1222
|
+
// The budget this call ENFORCED, always — it is the number the fitting below works
|
|
1223
|
+
// to, and it is measured with this field already present. Deferring to a panel-supplied
|
|
1224
|
+
// `max_chars` would report a bound nothing checked (codex gate SEVERE): a reply fitted
|
|
1225
|
+
// to 4000 could announce 3777 and look compliant at 3900.
|
|
1226
|
+
//
|
|
1227
|
+
// The loop above drops any INBOUND copy of the fields this function authors, for the
|
|
1228
|
+
// same reason (independent gate P0). An arriving `budget_overrun` is a claim from a
|
|
1229
|
+
// different measurement: carried through, a reply that fits after fitting would still
|
|
1230
|
+
// announce that it does not — a false positive on the one marker that has to be
|
|
1231
|
+
// trustworthy, and a caller who learns to distrust it will ignore the real ones. This
|
|
1232
|
+
// accounting is the sole author of these fields, so it is also their sole source.
|
|
1233
|
+
answer.max_chars = budget;
|
|
1234
|
+
const groups = Array.isArray(riders.groups) ? riders.groups : null;
|
|
1235
|
+
const hasGroups = !!groups && groups.length > 0;
|
|
1236
|
+
const hasRails = riders.rails !== undefined;
|
|
1237
|
+
/** The answer plus whichever riders `r` still carries, riders last. */
|
|
1238
|
+
const assemble = (r) => {
|
|
1239
|
+
const p = { ...answer };
|
|
1240
|
+
for (const k of QUERY_GRAPH_RIDER_KEYS)
|
|
1241
|
+
if (r[k] !== undefined)
|
|
1242
|
+
p[k] = r[k];
|
|
1243
|
+
return p;
|
|
1244
|
+
};
|
|
1245
|
+
const full = assemble(riders);
|
|
1246
|
+
const fullChars = sizeOf(full);
|
|
1247
|
+
if (fullChars <= budget)
|
|
1248
|
+
return replaceWith(full);
|
|
1249
|
+
/**
|
|
1250
|
+
* How the caller gets shed context back, judged against where they actually are.
|
|
1251
|
+
*
|
|
1252
|
+
* `needed` is the size of the reply that still carries it; `floor` is that same reply
|
|
1253
|
+
* with NO matching rows at all — the smallest this call could ever be while keeping
|
|
1254
|
+
* it. Both are MEASURED, not estimated, which is what lets each branch below be true:
|
|
1255
|
+
* narrowing the query can only help while `floor` fits, and raising `max_chars` can
|
|
1256
|
+
* only help while `needed` (or, combined with narrowing, `floor`) is under the
|
|
1257
|
+
* ceiling. Every other phrasing is a dead retry — a real lever that cannot move,
|
|
1258
|
+
* which costs the same round trip as naming one that does not exist.
|
|
1259
|
+
*
|
|
1260
|
+
* Response FIELDS are named bare on purpose: backticks here mean "a lever on this
|
|
1261
|
+
* tool", and dressing a field as a parameter is that same wasted retry.
|
|
1262
|
+
*/
|
|
1263
|
+
//
|
|
1264
|
+
// And the ROWS IN HAND may already be a cut set. The panel stops adding rows at this
|
|
1265
|
+
// same `max_chars` and reports `truncated_by:"max_chars"` when it did, so on that path
|
|
1266
|
+
// raising the budget returns MORE ROWS as well — a budget sized to "these rows plus
|
|
1267
|
+
// the riders" is then not enough to keep both, and calling that size "the untruncated
|
|
1268
|
+
// reply" describes a reply nobody has seen (codex gate MAJOR).
|
|
1269
|
+
const rowsCutByBudget = answer.truncated_by === "max_chars";
|
|
1270
|
+
const recovery = (needed, floor) => {
|
|
1271
|
+
const narrow = floor <= budget
|
|
1272
|
+
? " Narrowing this query (`ids`/`types`/`where`/`limit`) frees budget for them too."
|
|
1273
|
+
: "";
|
|
1274
|
+
const alsoMoreRows = rowsCutByBudget
|
|
1275
|
+
? " But the panel cut the ROWS at this same budget too, so raising it returns more rows as well and may still not leave room for them — narrow the query (`ids`/`types`/`where`/`limit`) in the same call to be sure."
|
|
1276
|
+
: narrow;
|
|
1277
|
+
if (budget >= QUERY_GRAPH_MAX_CHARS_CEILING) {
|
|
1278
|
+
return floor <= budget
|
|
1279
|
+
? `\`max_chars\` is already at its ceiling of ${QUERY_GRAPH_MAX_CHARS_CEILING}.${narrow}`
|
|
1280
|
+
: `\`max_chars\` is already at its ceiling of ${QUERY_GRAPH_MAX_CHARS_CEILING}, and they need ~${floor} chars even with no matching rows at all, so one reply cannot carry them.`;
|
|
1281
|
+
}
|
|
1282
|
+
if (needed <= QUERY_GRAPH_MAX_CHARS_CEILING)
|
|
1283
|
+
return `Keeping them alongside THESE rows takes ~${needed} chars: raise \`max_chars\` (up to ${QUERY_GRAPH_MAX_CHARS_CEILING}) to about that.${alsoMoreRows}`;
|
|
1284
|
+
if (floor <= QUERY_GRAPH_MAX_CHARS_CEILING)
|
|
1285
|
+
return `Keeping them alongside THESE rows takes ~${needed} chars, past this tool's ceiling — but only ~${floor} chars without the rows, so raise \`max_chars\` (up to ${QUERY_GRAPH_MAX_CHARS_CEILING}) AND narrow the query (\`ids\`/\`types\`/\`where\`/\`limit\`) together.`;
|
|
1286
|
+
return `They need ~${floor} chars even with no matching rows at all, past \`max_chars\`'s ceiling of ${QUERY_GRAPH_MAX_CHARS_CEILING}, so no combination of raising it and narrowing this query returns them here.`;
|
|
1287
|
+
};
|
|
1288
|
+
/** The same reply with the answer rows removed — the floor `recovery` reasons about. */
|
|
1289
|
+
const floorOf = (r) => sizeOf({ ...assemble(r), ...(answer.text !== undefined ? { text: "" } : {}) });
|
|
1290
|
+
const outline = ` panel_graph_outline's GROUPS index lists every member id — but only while its own max_chars affords a node-level rung; if it reports detail_level:"groups" it gives counts, not ids.`;
|
|
1291
|
+
// Every note below states what was OBSERVED — that the reply did not fit — and never
|
|
1292
|
+
// that dropping this made it fit (codex gate MAJOR). Whether it ultimately did is
|
|
1293
|
+
// answered by the presence or absence of `budget_overrun`, which is checked, not
|
|
1294
|
+
// predicted.
|
|
1295
|
+
// The panel caps its own groups list at 200 BEFORE this code sees it, so the coverage
|
|
1296
|
+
// claim any note here can make is about what the reply CARRIED, never about the graph
|
|
1297
|
+
// (codex gate MAJOR — "every group is still listed" sat next to the panel's own
|
|
1298
|
+
// "showing 200 of 640" and contradicted it).
|
|
1299
|
+
const groupsPreCapped = riders.groups_truncated === true;
|
|
1300
|
+
// Rung 1 — every group the reply carried still listed, membership and geometry gone.
|
|
1301
|
+
if (hasGroups) {
|
|
1302
|
+
const reduced = { ...riders, groups: groups.map(groupIndexEntry) };
|
|
1303
|
+
const p = assemble(reduced);
|
|
1304
|
+
p.groups_membership_omitted =
|
|
1305
|
+
`Member node_ids and box geometry were omitted from all ${groups.length} group(s): the whole reply did not fit \`max_chars\`=${budget}, ` +
|
|
1306
|
+
`and the rows answering your query are never dropped to make room for context. ` +
|
|
1307
|
+
(groupsPreCapped
|
|
1308
|
+
? `Every group this reply CARRIED is still listed with an exact node_count — but the panel had already capped that list before this call saw it (see groups_truncation_hint), so ${groups.length} is not the graph's total. `
|
|
1309
|
+
: `Every group is still listed and each node_count is exact. `) +
|
|
1310
|
+
recovery(fullChars, floorOf(riders)) +
|
|
1311
|
+
outline;
|
|
1312
|
+
if (sizeOf(p) <= budget)
|
|
1313
|
+
return replaceWith(p);
|
|
1314
|
+
}
|
|
1315
|
+
// The panel's OWN cap markers are evidence about the GRAPH, not payload this code is
|
|
1316
|
+
// free to spend: `groups_truncation_hint` is the only place the real group count
|
|
1317
|
+
// ("Showing 200 of 640") survives, so dropping it while dropping the list destroys the
|
|
1318
|
+
// one coverage fact the caller could still have had (codex gate MAJOR). They are two
|
|
1319
|
+
// short fields and they are kept on every rung below the index.
|
|
1320
|
+
const capEvidence = {};
|
|
1321
|
+
if (riders.groups_truncated !== undefined)
|
|
1322
|
+
capEvidence.groups_truncated = riders.groups_truncated;
|
|
1323
|
+
if (riders.groups_truncation_hint !== undefined)
|
|
1324
|
+
capEvidence.groups_truncation_hint = riders.groups_truncation_hint;
|
|
1325
|
+
// Rung 2 — the group index itself goes. Its true size is stated in its place.
|
|
1326
|
+
//
|
|
1327
|
+
// An EMPTY `groups: []` is not a rider to shed, it is an OBSERVATION — "this graph has
|
|
1328
|
+
// no groups" — costing about fifteen characters. Deleting it saved nothing and quietly
|
|
1329
|
+
// turned a stated zero into an absent field, which is the same coverage loss one rung
|
|
1330
|
+
// up (codex gate MAJOR). Only a non-empty list is dropped here.
|
|
1331
|
+
const withoutGroups = { ...riders };
|
|
1332
|
+
if (hasGroups)
|
|
1333
|
+
delete withoutGroups.groups;
|
|
1334
|
+
const noGroups = assemble(withoutGroups);
|
|
1335
|
+
if (hasGroups) {
|
|
1336
|
+
// The count we can vouch for is "what this reply carried", not "what the graph has".
|
|
1337
|
+
const carried = groupsPreCapped
|
|
1338
|
+
? `the ${groups.length} group(s) this reply carried (the panel had already capped that list, so that is not the graph's total — its own groups_truncated / groups_truncation_hint are kept here and record the real figure)`
|
|
1339
|
+
: `all ${groups.length} group(s)`;
|
|
1340
|
+
noGroups.groups_omitted =
|
|
1341
|
+
`The groups rider was dropped entirely — ${carried} — because the whole reply did not fit \`max_chars\`=${budget} even with their membership already omitted, ` +
|
|
1342
|
+
`and the rows answering your query are never dropped to make room for it. ` +
|
|
1343
|
+
recovery(fullChars, floorOf(riders)) +
|
|
1344
|
+
outline;
|
|
1345
|
+
}
|
|
1346
|
+
if (sizeOf(noGroups) <= budget)
|
|
1347
|
+
return replaceWith(noGroups);
|
|
1348
|
+
// Rung 3 — the subgraph rails go too. The cap evidence, and an empty groups
|
|
1349
|
+
// observation, still ride.
|
|
1350
|
+
const bareRiders = { ...capEvidence };
|
|
1351
|
+
if (!hasGroups && riders.groups !== undefined)
|
|
1352
|
+
bareRiders.groups = riders.groups;
|
|
1353
|
+
const bare = assemble(bareRiders);
|
|
1354
|
+
if (hasGroups && noGroups.groups_omitted !== undefined)
|
|
1355
|
+
bare.groups_omitted = noGroups.groups_omitted;
|
|
1356
|
+
if (hasRails) {
|
|
1357
|
+
bare.rails_omitted =
|
|
1358
|
+
`The subgraph boundary rails were dropped because the reply did not fit \`max_chars\`=${budget} without them. ` +
|
|
1359
|
+
recovery(sizeOf(noGroups), floorOf(withoutGroups));
|
|
1360
|
+
}
|
|
1361
|
+
// Every rider is gone and it STILL does not fit. What is left is the rows, the fields
|
|
1362
|
+
// that say which graph they came from, and the sentences saying what was dropped —
|
|
1363
|
+
// none of which may be silently cut, so the overrun is DISCLOSED with the real number
|
|
1364
|
+
// rather than left standing behind a `max_chars` that did not hold.
|
|
1365
|
+
//
|
|
1366
|
+
// WHICH part is responsible has to be measured, not assumed (codex gate MAJOR). The
|
|
1367
|
+
// earlier wording called every non-rider byte "the rows that answer your query" and
|
|
1368
|
+
// then offered to shrink them, which on a reply carrying a 3000-character subgraph
|
|
1369
|
+
// title blamed the wrong field AND named two levers that cannot touch it — the dead
|
|
1370
|
+
// retry this whole accounting exists to prevent. `lower max_chars` and `narrow the
|
|
1371
|
+
// query` both act on the ROWS ONLY, so they are offered only while the rest of the
|
|
1372
|
+
// reply would fit without them.
|
|
1373
|
+
const withoutRows = (p) => answer.text !== undefined ? { ...p, text: "" } : p;
|
|
1374
|
+
const rowsChars = render(answer).length - render(withoutRows(answer)).length;
|
|
1375
|
+
const floorWithNotes = sizeOf(withoutRows(bare));
|
|
1376
|
+
const shrink = floorWithNotes <= budget
|
|
1377
|
+
? budget > QUERY_GRAPH_MAX_CHARS_FLOOR
|
|
1378
|
+
? `Both levers act on the rows, so for a smaller reply lower \`max_chars\` (floor ${QUERY_GRAPH_MAX_CHARS_FLOOR}) or narrow the query with \`ids\`/\`types\`/\`where\`/\`limit\`.`
|
|
1379
|
+
: `\`max_chars\` is already at its floor of ${QUERY_GRAPH_MAX_CHARS_FLOOR}, so narrow the query with \`ids\`/\`types\`/\`where\`/\`limit\` for a smaller reply.`
|
|
1380
|
+
: `Lowering \`max_chars\` and narrowing the query both act on the ROWS ONLY, and everything else here is already ${floorWithNotes} chars on its own — so neither would bring this reply under the budget, and no parameter shrinks the rest.`;
|
|
1381
|
+
// "Nothing was discarded" is FALSE next to a groups_omitted note saying exactly what
|
|
1382
|
+
// was (codex gate r4). Two fields down, that contradiction is the kind a reader
|
|
1383
|
+
// resolves by distrusting both. Say which of the two situations this is.
|
|
1384
|
+
const shedSomething = hasGroups || hasRails;
|
|
1385
|
+
// Sibling text blocks are part of the reply and part of the figure, so they are named
|
|
1386
|
+
// rather than folded silently into "everything else" (independent gate P0).
|
|
1387
|
+
const siblings = siblingText > 0
|
|
1388
|
+
? `${res.content.filter((c, i) => i !== idx && c.type === "text").length} further text block(s) carrying ${siblingText} chars, `
|
|
1389
|
+
: "";
|
|
1390
|
+
const nonText = nonTextBlocks > 0
|
|
1391
|
+
? ` This reply also carries ${nonTextBlocks} non-text block(s), whose bytes \`max_chars\` does not bound and this figure does not count.`
|
|
1392
|
+
: "";
|
|
1393
|
+
const overrun = (size) => `This reply is ${size} chars, over \`max_chars\`=${budget} — that figure counts the JSON framing and escaping, every text block, and this note itself. ` +
|
|
1394
|
+
`Of it, the rows answering your query are ${rowsChars} chars; the remaining ${size - rowsChars} is ${siblings}the fields identifying the graph this came from, anything else the panel sent, and the note(s) above saying which context was dropped. ` +
|
|
1395
|
+
(shedSomething
|
|
1396
|
+
? `The context that COULD be dropped for the budget already has been, and the note(s) above record it; what remains was not cut down any further. `
|
|
1397
|
+
: `Nothing was discarded to meet the budget — there was no context to drop. `) +
|
|
1398
|
+
`The rows are your answer, and a silently short answer — or a silently missing rider, or a silently missing explanation of one — is the defect this accounting exists to prevent. ` +
|
|
1399
|
+
shrink +
|
|
1400
|
+
nonText;
|
|
1401
|
+
// The stated size must include the statement (codex gate MAJOR), so solve for it: the
|
|
1402
|
+
// note only grows the payload, and only its own digit count feeds back, so this
|
|
1403
|
+
// settles in one or two passes. The loop is bounded and the last pass is emitted
|
|
1404
|
+
// regardless — a size that is a character or two low is still the honest order of
|
|
1405
|
+
// magnitude, and the `budget_overrun` field itself is what carries the finding.
|
|
1406
|
+
let claimed = sizeOf({ ...bare, budget_overrun: overrun(sizeOf(bare)) });
|
|
1407
|
+
for (let i = 0; i < 4; i++) {
|
|
1408
|
+
const attempt = { ...bare, budget_overrun: overrun(claimed) };
|
|
1409
|
+
const size = sizeOf(attempt);
|
|
1410
|
+
if (size === claimed)
|
|
1411
|
+
return replaceWith(attempt);
|
|
1412
|
+
claimed = size;
|
|
1413
|
+
}
|
|
1414
|
+
return replaceWith({ ...bare, budget_overrun: overrun(claimed) });
|
|
1415
|
+
}
|
|
1090
1416
|
// ---- panel_civitai_results inline sample thumbnails (#623) -------------------
|
|
1091
1417
|
// The agent recommends CivitAI models/LoRAs for a VISUAL medium, so it must be
|
|
1092
1418
|
// able to SEE the sample images — not just read titles + download counts. The
|
|
@@ -2015,29 +2341,10 @@ function comfyWorkflowsDirs() {
|
|
|
2015
2341
|
join(base, "user", "workflows"),
|
|
2016
2342
|
];
|
|
2017
2343
|
}
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
* `fetchApi` throws for every non-2xx AND calls `response.json()` while building
|
|
2023
|
-
* that error — so a refusal with a non-JSON body (a `403 text/plain` from a proxy,
|
|
2024
|
-
* an HTML 502) rejects with a STATUSLESS SyntaxError. There is then no way to tell
|
|
2025
|
-
* "the server refused" from "the server was never reached", and the resolver's
|
|
2026
|
-
* local fallback would re-open for a server that had explicitly refused, loading a
|
|
2027
|
-
* different graph (#202).
|
|
2028
|
-
*
|
|
2029
|
-
* Building the request from the client's own `apiURL` + `apiHeaders` keeps every
|
|
2030
|
-
* transport concern identical (host, ssl, base path, clientId, Comfy-User, the
|
|
2031
|
-
* injected COMFYUI_AUTH_* fetch) while making the outcome unambiguous:
|
|
2032
|
-
* - a returned Response = the server ANSWERED; classify by status, decode later
|
|
2033
|
-
* - a thrown error = no Response exists, so the request never got one
|
|
2034
|
-
* Nothing here reads a body, so a body that cannot be decoded can never be
|
|
2035
|
-
* mistaken for a transport failure.
|
|
2036
|
-
*/
|
|
2037
|
-
async function userdataFetch(route) {
|
|
2038
|
-
const client = getClient();
|
|
2039
|
-
return await client.fetch(client.apiURL(route), { headers: client.apiHeaders() });
|
|
2040
|
-
}
|
|
2344
|
+
// The raw-Response userdata GET (why it bypasses `fetchApi`, and what a thrown error
|
|
2345
|
+
// vs a returned Response each prove) now lives in services/userdata-library.ts, shared
|
|
2346
|
+
// with list_workflows — see #810: the two callers had drifted, one recursing into
|
|
2347
|
+
// subfolders and one not.
|
|
2041
2348
|
/**
|
|
2042
2349
|
* Drop the one leading "workflows/" segment from CALLER input, leaving a key that
|
|
2043
2350
|
* is relative to the workflow store root.
|
|
@@ -2107,32 +2414,12 @@ const looseName = (name) => name.replace(/\\/g, "/").normalize("NFC").toLowerCas
|
|
|
2107
2414
|
* so this needs no version gate.
|
|
2108
2415
|
*/
|
|
2109
2416
|
async function listUserdataWorkflowKeys() {
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
return null;
|
|
2117
|
-
// Tolerate both shapes ComfyUI builds return: bare strings (the default on
|
|
2118
|
-
// 0.29.2), and {path} objects (what full_info=true emits, in case a build
|
|
2119
|
-
// returns that shape by default).
|
|
2120
|
-
return body
|
|
2121
|
-
.map((entry) => {
|
|
2122
|
-
if (typeof entry === "string")
|
|
2123
|
-
return entry;
|
|
2124
|
-
const rec = entry;
|
|
2125
|
-
if (rec && typeof rec.path === "string")
|
|
2126
|
-
return rec.path;
|
|
2127
|
-
if (rec && typeof rec.name === "string")
|
|
2128
|
-
return rec.name;
|
|
2129
|
-
return null;
|
|
2130
|
-
})
|
|
2131
|
-
.filter((k) => typeof k === "string" && k.length > 0);
|
|
2132
|
-
}
|
|
2133
|
-
catch {
|
|
2134
|
-
return null;
|
|
2135
|
-
}
|
|
2417
|
+
// ONE listing read for the whole server (#810): the recursive route, the entry-shape
|
|
2418
|
+
// tolerance and the status classification all live in services/userdata-library.ts.
|
|
2419
|
+
// This resolver only needs "keys, or nothing", so every unreadable flavour collapses
|
|
2420
|
+
// to null here — which is exactly what its refusal path already means.
|
|
2421
|
+
const listing = await listWorkflowLibraryKeys();
|
|
2422
|
+
return listing.ok ? listing.keys : null;
|
|
2136
2423
|
}
|
|
2137
2424
|
/** Validate a parsed value is a UI/litegraph workflow (a top-level `nodes`
|
|
2138
2425
|
* array), throwing a source-labelled error otherwise. */
|
|
@@ -3863,7 +4150,7 @@ export function buildPanelToolDefs() {
|
|
|
3863
4150
|
// Local helper so each def reads like the original `tool(...)` call.
|
|
3864
4151
|
const def = (name, description, schema, handler) => ({ name, description, schema, handler });
|
|
3865
4152
|
const defs = [
|
|
3866
|
-
def("panel_query_graph", "FILTER or TRAVERSE a SUBSET of the live canvas, for when you ALREADY KNOW what you're looking for. NOT for 'show me the canvas' or any whole-graph overview — call panel_graph_outline FIRST for that. NOT query_workflow (that queries a saved file or JSON you provide, not the live canvas). Filters, traverses, projects and aggregates over the workflow the user is CURRENTLY VIEWING without dumping the whole graph (replaces the old panel_get_graph full-JSON dump; output is TOKEN-BOUNDED with an explicit truncation marker, so a big graph can never flood your context). Combine: `types` (node type contains any), `title` (contains), `where` widget predicates ANDed ('cfg>7', 'steps<=20', 'sampler_name=euler', 'text~sunset' — ops = != >= <= > < ~contains), `ids` (exact nodes — THE way to read ONE node's exact slot/widget detail: {ids:[42], fields:'detail'}), `upstream_of`/`downstream_of` + `depth` (dependency traversal: upstream = what FEEDS that node, downstream = what CONSUMES it; seed at depth 0), `fields` ('compact' one line per node [default], 'ids', 'detail' = the full node summary with slots + connections + mode), `group_by:'type'` (counts only), `limit` (default 40). detail rows include each node's MODE — a 'bypass' node is skipped and a 'mute' node kills everything downstream, so check modes on the path you care about before running (fix with panel_set_node_mode). Every result also carries `groups` (id, title, member node_ids — groups are geometric, trust this list) and, when viewing a SUBGRAPH (after panel_enter_subgraph), `rails` (boundary rail ids/slots).
|
|
4153
|
+
def("panel_query_graph", "FILTER or TRAVERSE a SUBSET of the live canvas, for when you ALREADY KNOW what you're looking for. NOT for 'show me the canvas' or any whole-graph overview — call panel_graph_outline FIRST for that. NOT query_workflow (that queries a saved file or JSON you provide, not the live canvas). Filters, traverses, projects and aggregates over the workflow the user is CURRENTLY VIEWING without dumping the whole graph (replaces the old panel_get_graph full-JSON dump; output is TOKEN-BOUNDED with an explicit truncation marker, so a big graph can never flood your context). Combine: `types` (node type contains any), `title` (contains), `where` widget predicates ANDed ('cfg>7', 'steps<=20', 'sampler_name=euler', 'text~sunset' — ops = != >= <= > < ~contains), `ids` (exact nodes — THE way to read ONE node's exact slot/widget detail: {ids:[42], fields:'detail'}), `upstream_of`/`downstream_of` + `depth` (dependency traversal: upstream = what FEEDS that node, downstream = what CONSUMES it; seed at depth 0), `fields` ('compact' one line per node [default], 'ids', 'detail' = the full node summary with slots + connections + mode), `group_by:'type'` (counts only), `limit` (default 40). detail rows include each node's MODE — a 'bypass' node is skipped and a 'mute' node kills everything downstream, so check modes on the path you care about before running (fix with panel_set_node_mode). Every result also carries `groups` (id, title, member node_ids — groups are geometric, trust this list) and, when viewing a SUBGRAPH (after panel_enter_subgraph), `rails` (boundary rail ids/slots). `max_chars` bounds the WHOLE result, those riders included, and the rows you asked for are spent first: on a big graph the riders lose their member ids, then drop out entirely, rather than starving your query — and each says in-band that it did, with the true counts. Typical flow: panel_graph_outline to orient → panel_query_graph to pinpoint/inspect → edit. Read-only.", {
|
|
3867
4154
|
types: z.array(z.string()).optional().describe("Node type contains ANY of these (case-insensitive)."),
|
|
3868
4155
|
title: z.string().optional().describe("Node title contains this."),
|
|
3869
4156
|
where: z
|
|
@@ -3897,12 +4184,17 @@ export function buildPanelToolDefs() {
|
|
|
3897
4184
|
max_chars: z
|
|
3898
4185
|
.number()
|
|
3899
4186
|
.int()
|
|
3900
|
-
.min(
|
|
3901
|
-
.max(
|
|
4187
|
+
.min(QUERY_GRAPH_MAX_CHARS_FLOOR)
|
|
4188
|
+
.max(QUERY_GRAPH_MAX_CHARS_CEILING)
|
|
3902
4189
|
.optional()
|
|
3903
|
-
.describe(
|
|
3904
|
-
"
|
|
3905
|
-
},
|
|
4190
|
+
.describe(`Character bound for the WHOLE result, not just its rows (default ${QUERY_GRAPH_MAX_CHARS_DEFAULT}, max ${QUERY_GRAPH_MAX_CHARS_CEILING}). Raise only for deliberate full reads, e.g. layout passes needing every node's geometry. ` +
|
|
4191
|
+
"The rows answering your query are spent FIRST and are never dropped to fit the contextual groups/rails riders; those are spent from what is left and say in-band when they were reduced or omitted (#807)."),
|
|
4192
|
+
},
|
|
4193
|
+
// #807: the reply is fitted to `max_chars` as a WHOLE here — see fitQueryGraphReply.
|
|
4194
|
+
// The panel bounds `text`; the `groups`/`rails` riders were never in that
|
|
4195
|
+
// accounting, so on a large graph the reply could be many times the budget it
|
|
4196
|
+
// announced. Nothing is shed while the whole reply fits.
|
|
4197
|
+
async (args, ctx) => fitQueryGraphReply(await ctx.call({
|
|
3906
4198
|
cmd: "graph_query",
|
|
3907
4199
|
types: args.types,
|
|
3908
4200
|
title: args.title,
|
|
@@ -3915,7 +4207,7 @@ export function buildPanelToolDefs() {
|
|
|
3915
4207
|
group_by: args.group_by,
|
|
3916
4208
|
limit: args.limit,
|
|
3917
4209
|
max_chars: args.max_chars,
|
|
3918
|
-
})),
|
|
4210
|
+
}), args.max_chars)),
|
|
3919
4211
|
def("panel_graph_outline", "READ THE LIVE CANVAS the user is looking at, as text. 'Show me what's on the canvas' / 'what's on the graph right now' / 'read the current workflow' / 'describe the open graph' -> THIS TOOL, with no arguments. NOT visualize_workflow or visualize_workflow_hierarchical (those DRAW A DIAGRAM of a workflow you PASS IN — a saved file or JSON — and never see the live canvas). NOT panel_query_graph (that FILTERS a SUBSET, for when you already know what you're looking for). Returns one `outline` string covering the WHOLE open graph, topologically sorted (sources first, sinks last): each node as `id Type \"title\" [bypass/mute] [OUTPUT] · group:X widget=value …` with `← inputs` (source_node.output_name) and `→ outputs` (target_node.input_name), after a GROUPS index (title → member node ids). It gives you the WIRING you would otherwise reconstruct by hand — read it FIRST to get oriented, then panel_query_graph to inspect one node ({ids:[42], fields:'detail'}) or panel_find_nodes for free-text search. Over `max_chars` it never cuts the graph short: it sheds per-node detail, or refuses with a reason — never a partial outline. Read-only.", {
|
|
3920
4212
|
max_chars: z
|
|
3921
4213
|
.number()
|