comfyui-mcp 0.52.91 → 0.52.93
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.
|
@@ -27,13 +27,13 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
27
27
|
// #873 — the operator's tool-surface policy also governs the panel surface.
|
|
28
28
|
import { resolveToolSurfacePolicy, toolActionPolicyError, toolAllowed, } from "../tools/tool-surface-filter.js";
|
|
29
29
|
import { logger as toolPolicyLogger } from "../utils/logger.js";
|
|
30
|
-
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
30
|
+
import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
31
31
|
import { forwardedByReferenceNote, oversizedInlineRefusal, resolveServableViewRef, stageFileIntoServedDir, stagedForDisplayNote, readShowMediaAck, unaccountedShowMediaNote, unverifiedViewRefNote, } from "../services/comfy-view-ref.js";
|
|
32
32
|
import { extname, isAbsolute, join, resolve, sep } from "node:path";
|
|
33
33
|
import { fileURLToPath } from "node:url";
|
|
34
34
|
import { comfyuiFetch } from "../comfyui/fetch.js";
|
|
35
35
|
import { assertPanelNotTargetedUnverifiable } from "../services/panel-pin-guard.js";
|
|
36
|
-
import { nodesInstallCommandArgs } from "../services/node-management.js";
|
|
36
|
+
import { findPackOnDisk, nodesInstallCommandArgs, } from "../services/node-management.js";
|
|
37
37
|
import { sanitizePanelUpdateNodeResult } from "../services/manager-update-error.js";
|
|
38
38
|
import { formatQueueStatusPartialNote, getManifestPartialLeftover, } from "../services/manifest-partial.js";
|
|
39
39
|
import { searchPanelNodes } from "../services/manager-node-search.js";
|
|
@@ -183,7 +183,7 @@ import { describeUnappliedFilters } from "./civitai-filter-guard.js";
|
|
|
183
183
|
import { recordTodo, normalizeTodoItems, TODO_STATUS_INPUTS } from "./todo-state.js";
|
|
184
184
|
import { applyCapturedWidgetValues } from "../services/live-widget-overlay.js";
|
|
185
185
|
import { listWorkflowLibraryKeys, userdataFetch } from "../services/userdata-library.js";
|
|
186
|
-
import { resolveEffectiveComfyUIBase } from "../services/workspace-env.js";
|
|
186
|
+
import { resolveCustomNodesScanBaseLive, resolveEffectiveComfyUIBase, } from "../services/workspace-env.js";
|
|
187
187
|
import { getNsfwConsent, setNsfwConsent } from "../services/panel-settings.js";
|
|
188
188
|
import { QueueMonitor } from "../services/queue-monitor.js";
|
|
189
189
|
import { RunCompletions } from "./run-completion-journal.js";
|
|
@@ -3215,11 +3215,256 @@ function panelIncarnation(ctx, tabId) {
|
|
|
3215
3215
|
const b = ctx.bridge;
|
|
3216
3216
|
return typeof b?.tabIncarnation === "function" ? b.tabIncarnation(tabId) : undefined;
|
|
3217
3217
|
}
|
|
3218
|
-
|
|
3219
|
-
|
|
3218
|
+
function registryInstallId(args) {
|
|
3219
|
+
// The normalized command is the authority here. In particular, a repository
|
|
3220
|
+
// URL that arrived in `id` has already been rerouted to `repository` and must
|
|
3221
|
+
// not enter this local registry-only fallback.
|
|
3222
|
+
if (args.repository !== undefined || typeof args.id !== "string")
|
|
3223
|
+
return null;
|
|
3224
|
+
const id = args.id.trim();
|
|
3225
|
+
if (!id || /[/\\\x00-\x1F\x7F]/.test(id) || /^[a-z][a-z0-9+.-]*:\/\//i.test(id)) {
|
|
3226
|
+
return null;
|
|
3227
|
+
}
|
|
3228
|
+
return id;
|
|
3229
|
+
}
|
|
3230
|
+
/** A path comparison that does not let case or slash spelling retarget evidence on Windows. */
|
|
3231
|
+
function sameFilesystemPath(a, b) {
|
|
3232
|
+
try {
|
|
3233
|
+
const normalize = (value) => resolve(value).replace(/[\\/]+$/, "");
|
|
3234
|
+
const left = normalize(a);
|
|
3235
|
+
const right = normalize(b);
|
|
3236
|
+
return process.platform === "win32"
|
|
3237
|
+
? left.toLowerCase() === right.toLowerCase()
|
|
3238
|
+
: left === right;
|
|
3239
|
+
}
|
|
3240
|
+
catch {
|
|
3241
|
+
return false;
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
/**
|
|
3245
|
+
* Strict post-install evidence for the registry-only fallback.
|
|
3246
|
+
*
|
|
3247
|
+
* `findPackOnDisk` is intentionally broader because other node-management
|
|
3248
|
+
* operations must recognize renamed and disabled packs. This fallback needs a
|
|
3249
|
+
* narrower answer: the exact, newly-created, enabled directory, with Manager's
|
|
3250
|
+
* tracking marker and readable Python/package metadata. A symlink, marker-only
|
|
3251
|
+
* husk, arbitrary same-name folder, or any read error is inconclusive.
|
|
3252
|
+
*/
|
|
3253
|
+
function inspectNewRegistryPack(id, scanBase) {
|
|
3254
|
+
const customNodes = join(scanBase, "custom_nodes");
|
|
3255
|
+
try {
|
|
3256
|
+
const entries = readdirSync(customNodes, { withFileTypes: true });
|
|
3257
|
+
const wanted = id.toLowerCase();
|
|
3258
|
+
const entry = entries.find((candidate) => candidate.name.toLowerCase() === wanted);
|
|
3259
|
+
if (!entry || entry.isSymbolicLink() || !entry.isDirectory()) {
|
|
3260
|
+
return { state: "not-found", scanned: customNodes };
|
|
3261
|
+
}
|
|
3262
|
+
const dir = join(customNodes, entry.name);
|
|
3263
|
+
const lstat = lstatSync(dir);
|
|
3264
|
+
if (!lstat.isDirectory() || lstat.isSymbolicLink()) {
|
|
3265
|
+
return { state: "not-found", scanned: customNodes };
|
|
3266
|
+
}
|
|
3267
|
+
// Reject junctions and other directory indirections too. The panel's local
|
|
3268
|
+
// proof must describe the directory Manager just populated, not another tree.
|
|
3269
|
+
if (!sameFilesystemPath(realpathSync(dir), dir)) {
|
|
3270
|
+
return { state: "not-found", scanned: customNodes };
|
|
3271
|
+
}
|
|
3272
|
+
const files = readdirSync(dir, { withFileTypes: true });
|
|
3273
|
+
const tracking = files.find((file) => file.name === ".tracking" && file.isFile());
|
|
3274
|
+
const code = files.find((file) => file.isFile() &&
|
|
3275
|
+
(file.name.toLowerCase() === "__init__.py" ||
|
|
3276
|
+
file.name.toLowerCase() === "pyproject.toml" ||
|
|
3277
|
+
file.name.toLowerCase().endsWith(".py")));
|
|
3278
|
+
if (!tracking || !code)
|
|
3279
|
+
return { state: "not-found", scanned: customNodes };
|
|
3280
|
+
// Reading both the Manager marker and the loadable file makes an access
|
|
3281
|
+
// failure a refusal, never a truthy `looksLikeAPack` fallback.
|
|
3282
|
+
readFileSync(join(dir, tracking.name));
|
|
3283
|
+
readFileSync(join(dir, code.name));
|
|
3284
|
+
return { state: "found", dir };
|
|
3285
|
+
}
|
|
3286
|
+
catch (err) {
|
|
3287
|
+
return {
|
|
3288
|
+
state: "unreadable",
|
|
3289
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
3290
|
+
};
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
async function captureRegistryInstallSnapshot(args, panelBaseBeforeDispatch) {
|
|
3294
|
+
const id = registryInstallId(args);
|
|
3295
|
+
if (!id || !panelBaseBeforeDispatch)
|
|
3296
|
+
return null;
|
|
3297
|
+
try {
|
|
3298
|
+
const scanBase = await resolveCustomNodesScanBaseLive({ requireLive: true });
|
|
3299
|
+
if (!scanBase)
|
|
3300
|
+
return null;
|
|
3301
|
+
return { id, scanBase, before: findPackOnDisk(id, scanBase) };
|
|
3302
|
+
}
|
|
3303
|
+
catch {
|
|
3304
|
+
return null;
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
function queueStatusRecord(reply) {
|
|
3308
|
+
if (!reply)
|
|
3309
|
+
return null;
|
|
3310
|
+
const nested = reply.status;
|
|
3311
|
+
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
3312
|
+
// Manager responses can put queue counters under `status` while leaving
|
|
3313
|
+
// failure metadata (for example recent_failures) on the outer envelope.
|
|
3314
|
+
// Flatten both records so the settlement gate cannot discard that evidence.
|
|
3315
|
+
return { ...reply, ...nested };
|
|
3316
|
+
}
|
|
3317
|
+
return reply;
|
|
3318
|
+
}
|
|
3319
|
+
function explicitQueueFailure(value) {
|
|
3320
|
+
if (value === true)
|
|
3321
|
+
return true;
|
|
3322
|
+
if (typeof value === "number")
|
|
3323
|
+
return Number.isFinite(value) && value > 0;
|
|
3324
|
+
if (typeof value === "string")
|
|
3325
|
+
return value.trim() !== "";
|
|
3326
|
+
if (Array.isArray(value))
|
|
3327
|
+
return value.length > 0;
|
|
3328
|
+
return value !== null && typeof value === "object";
|
|
3329
|
+
}
|
|
3330
|
+
function queueReportsFailure(status) {
|
|
3331
|
+
// These are the protocol's explicit failure payloads. Empty lists/strings and
|
|
3332
|
+
// false flags are not failures; every non-empty/error-shaped value is.
|
|
3333
|
+
for (const key of [
|
|
3334
|
+
"failed",
|
|
3335
|
+
"error",
|
|
3336
|
+
"failure",
|
|
3337
|
+
"failures",
|
|
3338
|
+
"recent_failures",
|
|
3339
|
+
"errors",
|
|
3340
|
+
]) {
|
|
3341
|
+
if (explicitQueueFailure(status[key]))
|
|
3342
|
+
return true;
|
|
3343
|
+
}
|
|
3344
|
+
for (const key of [
|
|
3345
|
+
"failed_count",
|
|
3346
|
+
"failure_count",
|
|
3347
|
+
"error_count",
|
|
3348
|
+
"fail_count",
|
|
3349
|
+
]) {
|
|
3350
|
+
if (typeof status[key] === "number" && status[key] > 0)
|
|
3351
|
+
return true;
|
|
3352
|
+
}
|
|
3353
|
+
if (typeof status.failure_reporting === "string") {
|
|
3354
|
+
const reporting = status.failure_reporting.trim().toLowerCase();
|
|
3355
|
+
if (reporting === "failed" || reporting === "failure" || reporting === "error") {
|
|
3356
|
+
return true;
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
const state = typeof status.status === "string" ? status.status.toLowerCase() : "";
|
|
3360
|
+
return state === "failed" || state === "failure" || state === "error";
|
|
3361
|
+
}
|
|
3362
|
+
function queueSettledForDiskCorroboration(status, outerReply) {
|
|
3363
|
+
if (!status || queueReportsFailure(status) || (outerReply && queueReportsFailure(outerReply))) {
|
|
3364
|
+
return false;
|
|
3365
|
+
}
|
|
3366
|
+
if (status.is_processing !== false)
|
|
3367
|
+
return false;
|
|
3368
|
+
for (const key of ["pending_count", "in_progress_count"]) {
|
|
3369
|
+
if (typeof status[key] === "number" && status[key] !== 0)
|
|
3370
|
+
return false;
|
|
3371
|
+
}
|
|
3372
|
+
// A legacy zero-total idle snapshot is the known dropped-enqueue signature;
|
|
3373
|
+
// settleDroppedEnqueue turns that into a warning and blocks this path. A v4
|
|
3374
|
+
// idle snapshot has no total_count, so the strict disk delta remains the
|
|
3375
|
+
// available post-install evidence.
|
|
3376
|
+
if (status.total_count === 0)
|
|
3377
|
+
return false;
|
|
3378
|
+
if (typeof status.total_count === "number" &&
|
|
3379
|
+
(typeof status.done_count !== "number" || status.done_count <= 0)) {
|
|
3380
|
+
return false;
|
|
3381
|
+
}
|
|
3382
|
+
return typeof status.pending_count === "number" ||
|
|
3383
|
+
(typeof status.total_count === "number" && status.total_count > 0) ||
|
|
3384
|
+
(typeof status.done_count === "number" && status.done_count > 0);
|
|
3385
|
+
}
|
|
3386
|
+
/**
|
|
3387
|
+
* #2180 — Panel 0.15.71 can report a registry install as unverified when a
|
|
3388
|
+
* registry zip lands in custom_nodes without entering the installed list.
|
|
3389
|
+
*
|
|
3390
|
+
* The fallback is deliberately narrower than the normal node-management disk
|
|
3391
|
+
* scan: it can only corroborate a registry id whose target was absent in a
|
|
3392
|
+
* pre-dispatch snapshot, after the queued response has passed queue settlement,
|
|
3393
|
+
* and only when a strict new-pack read succeeds on the same live target.
|
|
3394
|
+
*/
|
|
3395
|
+
async function corroborateUntrackedRegistryInstall(ctx, res, snapshot, dispatch, panelBaseBeforeDispatch, mayCorroborateDisk) {
|
|
3396
|
+
if (!mayCorroborateDisk || res.isError || !snapshot || snapshot.before.state !== "not-found") {
|
|
3220
3397
|
return res;
|
|
3221
|
-
|
|
3398
|
+
}
|
|
3399
|
+
const parsed = parseToolResultJson(res);
|
|
3400
|
+
if (!parsed ||
|
|
3401
|
+
parsed.installed !== false ||
|
|
3402
|
+
parsed.verified !== false ||
|
|
3403
|
+
!claimsQueued(parsed) ||
|
|
3404
|
+
parsed.failed === true ||
|
|
3405
|
+
parsed.error === true) {
|
|
3406
|
+
return res;
|
|
3407
|
+
}
|
|
3408
|
+
// Require the panel response to echo the same registry identity. A fresh
|
|
3409
|
+
// directory with only a caller-supplied same-name id is not proof that the
|
|
3410
|
+
// Manager installed that id.
|
|
3411
|
+
if (typeof parsed.id !== "string" ||
|
|
3412
|
+
parsed.id.trim().toLowerCase() !== snapshot.id.toLowerCase()) {
|
|
3413
|
+
return res;
|
|
3414
|
+
}
|
|
3415
|
+
// The follow-up must still be attributable to the panel and server that were
|
|
3416
|
+
// captured before dispatch. This mirrors settleDroppedEnqueue's takeover guard
|
|
3417
|
+
// and keeps local disk evidence inside the requireLive/security scope.
|
|
3418
|
+
if (ctx.tabId !== dispatch.tab || dispatch.incarnation === undefined)
|
|
3419
|
+
return res;
|
|
3420
|
+
if (panelIncarnation(ctx, ctx.tabId) !== dispatch.incarnation)
|
|
3421
|
+
return res;
|
|
3422
|
+
if (!panelBaseBeforeDispatch || !sameHttpBase(getComfyUIBaseUrl(), panelBaseBeforeDispatch)) {
|
|
3423
|
+
return res;
|
|
3424
|
+
}
|
|
3425
|
+
let liveScanBase;
|
|
3426
|
+
try {
|
|
3427
|
+
liveScanBase = await resolveCustomNodesScanBaseLive({ requireLive: true });
|
|
3428
|
+
}
|
|
3429
|
+
catch {
|
|
3222
3430
|
return res;
|
|
3431
|
+
}
|
|
3432
|
+
if (!liveScanBase || !sameFilesystemPath(liveScanBase, snapshot.scanBase))
|
|
3433
|
+
return res;
|
|
3434
|
+
const disk = inspectNewRegistryPack(snapshot.id, snapshot.scanBase);
|
|
3435
|
+
if (disk.state !== "found")
|
|
3436
|
+
return res;
|
|
3437
|
+
const upgraded = {
|
|
3438
|
+
...parsed,
|
|
3439
|
+
installed: true,
|
|
3440
|
+
verified: true,
|
|
3441
|
+
restart_required: true,
|
|
3442
|
+
verification_evidence: "new-on-disk-registry-pack",
|
|
3443
|
+
note: [
|
|
3444
|
+
typeof parsed.note === "string" ? parsed.note : undefined,
|
|
3445
|
+
`The pre-install snapshot found no matching pack, and after queue settlement a readable enabled registry pack with Manager tracking metadata appeared at ${disk.dir}.`,
|
|
3446
|
+
"The disk evidence does not establish the requested version or channel; those response fields were preserved unchanged. Restart ComfyUI before relying on the new nodes.",
|
|
3447
|
+
]
|
|
3448
|
+
.filter(Boolean)
|
|
3449
|
+
.join(" "),
|
|
3450
|
+
};
|
|
3451
|
+
return {
|
|
3452
|
+
...res,
|
|
3453
|
+
content: [
|
|
3454
|
+
{ type: "text", text: JSON.stringify(upgraded, null, 2) },
|
|
3455
|
+
...res.content.slice(1),
|
|
3456
|
+
],
|
|
3457
|
+
...(res.structuredContent
|
|
3458
|
+
? { structuredContent: { ...res.structuredContent, ...upgraded } }
|
|
3459
|
+
: {}),
|
|
3460
|
+
};
|
|
3461
|
+
}
|
|
3462
|
+
async function settleDroppedEnqueue(ctx, res, dispatch) {
|
|
3463
|
+
if (res.isError)
|
|
3464
|
+
return { result: res, mayCorroborateDisk: false };
|
|
3465
|
+
if (!claimsQueued(parseToolResultJson(res))) {
|
|
3466
|
+
return { result: res, mayCorroborateDisk: false };
|
|
3467
|
+
}
|
|
3223
3468
|
// The queue is only evidence about the panel the install was DISPATCHED to
|
|
3224
3469
|
// (codex P1 — and the same guard #1468 needed, which I did not carry across).
|
|
3225
3470
|
// `ctx.call` runs ensureReachable first, which silently rebinds an unpinned
|
|
@@ -3233,7 +3478,7 @@ async function settleDroppedEnqueue(ctx, res, dispatch) {
|
|
|
3233
3478
|
// both are captured: the key AND the incarnation currently holding it.
|
|
3234
3479
|
const queue = await ctx.call({ cmd: "nodes_queue_status" }, 15000);
|
|
3235
3480
|
if (ctx.tabId !== dispatch.tab)
|
|
3236
|
-
return res;
|
|
3481
|
+
return { result: res, mayCorroborateDisk: false };
|
|
3237
3482
|
// Both captured BEFORE the install was dispatched, not here — a takeover that
|
|
3238
3483
|
// happens DURING the install is already baked in by the time this function
|
|
3239
3484
|
// runs, so comparing two post-install readings would always agree and the guard
|
|
@@ -3242,11 +3487,20 @@ async function settleDroppedEnqueue(ctx, res, dispatch) {
|
|
|
3242
3487
|
// rule out a same-key takeover, so it does not get to make a claim about which
|
|
3243
3488
|
// panel answered.
|
|
3244
3489
|
if (dispatch.incarnation === undefined)
|
|
3245
|
-
return res;
|
|
3246
|
-
if (panelIncarnation(ctx, ctx.tabId) !== dispatch.incarnation)
|
|
3247
|
-
return res;
|
|
3248
|
-
|
|
3249
|
-
|
|
3490
|
+
return { result: res, mayCorroborateDisk: false };
|
|
3491
|
+
if (panelIncarnation(ctx, ctx.tabId) !== dispatch.incarnation) {
|
|
3492
|
+
return { result: res, mayCorroborateDisk: false };
|
|
3493
|
+
}
|
|
3494
|
+
if (queue.isError)
|
|
3495
|
+
return { result: res, mayCorroborateDisk: false };
|
|
3496
|
+
const queueReply = parseToolResultJson(queue);
|
|
3497
|
+
const status = queueStatusRecord(queueReply);
|
|
3498
|
+
if (!queueNeverSawATask(queueReply)) {
|
|
3499
|
+
return {
|
|
3500
|
+
result: res,
|
|
3501
|
+
mayCorroborateDisk: queueSettledForDiskCorroboration(status, queueReply),
|
|
3502
|
+
};
|
|
3503
|
+
}
|
|
3250
3504
|
// NO PROVENANCE CLAIM AT ALL — deliberately, after five review rounds.
|
|
3251
3505
|
//
|
|
3252
3506
|
// Earlier drafts explained WHY the task was probably dropped ("the Manager
|
|
@@ -3260,19 +3514,22 @@ async function settleDroppedEnqueue(ctx, res, dispatch) {
|
|
|
3260
3514
|
// saying, needs no provenance, and cannot be wrong. The cause belongs to
|
|
3261
3515
|
// whoever can see the install — so the message asks for the ONE check that
|
|
3262
3516
|
// settles it and stops there. Less useful in the common case; never false.
|
|
3263
|
-
return
|
|
3264
|
-
`
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3517
|
+
return {
|
|
3518
|
+
result: appendNote(res, `WARNING — THE QUEUE DOES NOT HAVE THIS TASK. The Manager accepted the install above, but a ` +
|
|
3519
|
+
`read taken immediately afterwards, on that same panel, reports an IDLE queue holding no ` +
|
|
3520
|
+
`tasks at all (total_count, done, in_progress all 0). "queued" is its acknowledgement, not ` +
|
|
3521
|
+
`a receipt.\n\n` +
|
|
3522
|
+
`THIS IS NOT PROOF EITHER WAY. Those counters are also cleared by a queue RESET, which ` +
|
|
3523
|
+
`other operations in this server issue, so an install that really ran can read exactly ` +
|
|
3524
|
+
`like this if a reset landed in between.\n\n` +
|
|
3525
|
+
`SO CHECK: call panel_list_nodes and see whether the pack is actually there. Do not restart ` +
|
|
3526
|
+
`on the assumption it installed, and do not reinstall on the assumption it did not.\n\n` +
|
|
3527
|
+
`IF IT IS ABSENT: install it through the headless install_custom_node, which verifies the ` +
|
|
3528
|
+
`pack really landed instead of trusting the queue, and can clone a repository URL directly ` +
|
|
3529
|
+
`when the Manager will not take it. This tool cannot clone for you — it drives whatever ` +
|
|
3530
|
+
`ComfyUI the panel is bound to, which need not be this machine.`),
|
|
3531
|
+
mayCorroborateDisk: false,
|
|
3532
|
+
};
|
|
3276
3533
|
}
|
|
3277
3534
|
/**
|
|
3278
3535
|
* #1699 — a drained Manager queue is not apply_manifest completion when
|
|
@@ -15660,18 +15917,24 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
|
|
|
15660
15917
|
// #1129 — the panel identity is captured BEFORE dispatch, because a
|
|
15661
15918
|
// takeover during the install is exactly what the follow-up read must not
|
|
15662
15919
|
// be attributed to.
|
|
15920
|
+
// #2180 — the local disk corroboration below uses the same server-authorized
|
|
15921
|
+
// binding, captured before dispatch, so a reconnect cannot retarget it.
|
|
15922
|
+
const panelBaseBeforeDispatch = captureRebootHealthBase(ctx);
|
|
15663
15923
|
const dispatch = {
|
|
15664
15924
|
tab: ctx.tabId,
|
|
15665
15925
|
incarnation: panelIncarnation(ctx, ctx.tabId),
|
|
15666
15926
|
};
|
|
15927
|
+
// #2180 — this snapshot must happen BEFORE Manager dispatch. A post-only
|
|
15928
|
+
// scan cannot distinguish a newly landed registry zip from an old pack,
|
|
15929
|
+
// a concurrent writer, or a directory that merely shares the id.
|
|
15930
|
+
const registrySnapshot = await captureRegistryInstallSnapshot(cmdArgs, panelBaseBeforeDispatch);
|
|
15667
15931
|
const res = await ctx.call({ cmd: "nodes_install", ...cmdArgs }, 30000);
|
|
15668
|
-
// #1129 — settle
|
|
15669
|
-
//
|
|
15670
|
-
//
|
|
15671
|
-
|
|
15672
|
-
|
|
15673
|
-
|
|
15674
|
-
const settled = await settleDroppedEnqueue(ctx, res, dispatch);
|
|
15932
|
+
// #1129 / #2180 — settle EVERY queued/pending response before any disk
|
|
15933
|
+
// corroboration. An early rewrite would bypass the queue read and could
|
|
15934
|
+
// turn a still-running or failed request into installed:true.
|
|
15935
|
+
const settlement = await settleDroppedEnqueue(ctx, res, dispatch);
|
|
15936
|
+
const corroborated = await corroborateUntrackedRegistryInstall(ctx, settlement.result, registrySnapshot, dispatch, panelBaseBeforeDispatch, settlement.mayCorroborateDisk);
|
|
15937
|
+
const settled = corroborated;
|
|
15675
15938
|
if (note && !cmdArgs.repository) {
|
|
15676
15939
|
const text = settled.content.find((c) => c.type === "text");
|
|
15677
15940
|
if (text && text.type === "text") {
|