comfyui-mcp 0.52.75 → 0.52.76
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.
|
@@ -4297,6 +4297,8 @@ const ANIMA_REGIONAL_WIRED_PROMPT_INPUT = {
|
|
|
4297
4297
|
scene_prompt: "scene_prompt_in",
|
|
4298
4298
|
negative_prompt: "negative_prompt_in",
|
|
4299
4299
|
};
|
|
4300
|
+
const DASIWA_STACK_WIDGET = "stack_data";
|
|
4301
|
+
const DASIWA_LTX2_LORA_LOADER = "DaSiWa_LTX2LoraLoader";
|
|
4300
4302
|
function isAnimaRegionalPromptWidget(widget) {
|
|
4301
4303
|
return ANIMA_REGIONAL_PROMPT_WIDGETS.has(widget);
|
|
4302
4304
|
}
|
|
@@ -4338,6 +4340,101 @@ function parseQueriedNodeType(payload) {
|
|
|
4338
4340
|
}
|
|
4339
4341
|
return null;
|
|
4340
4342
|
}
|
|
4343
|
+
function canonicalQueriedNodeId(value) {
|
|
4344
|
+
if (typeof value === "number") {
|
|
4345
|
+
return Number.isSafeInteger(value) ? String(value) : null;
|
|
4346
|
+
}
|
|
4347
|
+
if (typeof value !== "string" || !NODE_ID_PATTERN.test(value))
|
|
4348
|
+
return null;
|
|
4349
|
+
const normalized = normalizeNodeId(value);
|
|
4350
|
+
return typeof normalized === "number" && !Number.isSafeInteger(normalized)
|
|
4351
|
+
? null
|
|
4352
|
+
: String(normalized);
|
|
4353
|
+
}
|
|
4354
|
+
function parseQueriedNodeIdentityRow(row) {
|
|
4355
|
+
if (!row || typeof row !== "object" || Array.isArray(row))
|
|
4356
|
+
return null;
|
|
4357
|
+
const record = row;
|
|
4358
|
+
const id = canonicalQueriedNodeId(record.id);
|
|
4359
|
+
if (!id)
|
|
4360
|
+
return null;
|
|
4361
|
+
const type = record.type;
|
|
4362
|
+
const classType = record.class_type;
|
|
4363
|
+
if (type !== undefined && typeof type !== "string")
|
|
4364
|
+
return null;
|
|
4365
|
+
if (classType !== undefined && typeof classType !== "string")
|
|
4366
|
+
return null;
|
|
4367
|
+
if (typeof type === "string" && typeof classType === "string" && type !== classType) {
|
|
4368
|
+
return null;
|
|
4369
|
+
}
|
|
4370
|
+
const resolvedType = typeof type === "string" ? type : classType;
|
|
4371
|
+
return resolvedType ? { id, type: resolvedType } : null;
|
|
4372
|
+
}
|
|
4373
|
+
/** Strict identity parser for the DaSiWa refusal gate. Unlike the older type-only
|
|
4374
|
+
* parser above, this accepts exactly one non-truncated row and authenticates its id. */
|
|
4375
|
+
function parseVerifiedQueriedNodeIdentity(payload) {
|
|
4376
|
+
if (!payload)
|
|
4377
|
+
return null;
|
|
4378
|
+
if (Object.prototype.hasOwnProperty.call(payload, "truncated") &&
|
|
4379
|
+
payload.truncated !== false) {
|
|
4380
|
+
return null;
|
|
4381
|
+
}
|
|
4382
|
+
if (Object.prototype.hasOwnProperty.call(payload, "nodes")) {
|
|
4383
|
+
return Array.isArray(payload.nodes) && payload.nodes.length === 1
|
|
4384
|
+
? parseQueriedNodeIdentityRow(payload.nodes[0])
|
|
4385
|
+
: null;
|
|
4386
|
+
}
|
|
4387
|
+
if (typeof payload.text !== "string")
|
|
4388
|
+
return null;
|
|
4389
|
+
const rows = [];
|
|
4390
|
+
const lines = payload.text.split(/\r?\n/);
|
|
4391
|
+
for (const [index, line] of lines.entries()) {
|
|
4392
|
+
const trimmed = line.trim();
|
|
4393
|
+
if (!trimmed)
|
|
4394
|
+
continue;
|
|
4395
|
+
// The live compact projection always prefixes its rows with this bounded summary.
|
|
4396
|
+
// Keep the grammar narrow so arbitrary prose cannot make an unverified row usable,
|
|
4397
|
+
// while accepting the documented header that older tests and panels may omit.
|
|
4398
|
+
if (index === 0 &&
|
|
4399
|
+
/^\d+ match\(es\) of \d+ in scope \(viewing: \d+ nodes\)(?: · traversal depth≤-?\d+)?$/.test(trimmed)) {
|
|
4400
|
+
continue;
|
|
4401
|
+
}
|
|
4402
|
+
// A compact row may be followed by one of the panel's bounded clipping notes or
|
|
4403
|
+
// truncation remedies. They do not identify nodes and are ignored only after a row
|
|
4404
|
+
// has been authenticated below.
|
|
4405
|
+
if (rows.length > 0 &&
|
|
4406
|
+
(/^\(\d+ widget value\(s\) clipped to \d+ chars/.test(trimmed) ||
|
|
4407
|
+
/^… truncated at \d+ of \d+/.test(trimmed))) {
|
|
4408
|
+
continue;
|
|
4409
|
+
}
|
|
4410
|
+
if (trimmed.startsWith("#")) {
|
|
4411
|
+
const compact = trimmed.match(/^#(\S+)\s+(\S+)(?:\s|$)/);
|
|
4412
|
+
if (!compact)
|
|
4413
|
+
return null;
|
|
4414
|
+
const id = canonicalQueriedNodeId(compact[1]);
|
|
4415
|
+
if (!id)
|
|
4416
|
+
return null;
|
|
4417
|
+
rows.push({ id, type: compact[2] });
|
|
4418
|
+
continue;
|
|
4419
|
+
}
|
|
4420
|
+
if (trimmed.startsWith("{")) {
|
|
4421
|
+
let row;
|
|
4422
|
+
try {
|
|
4423
|
+
row = JSON.parse(trimmed);
|
|
4424
|
+
}
|
|
4425
|
+
catch {
|
|
4426
|
+
return null;
|
|
4427
|
+
}
|
|
4428
|
+
const parsed = parseQueriedNodeIdentityRow(row);
|
|
4429
|
+
if (!parsed)
|
|
4430
|
+
return null;
|
|
4431
|
+
rows.push(parsed);
|
|
4432
|
+
continue;
|
|
4433
|
+
}
|
|
4434
|
+
return null;
|
|
4435
|
+
}
|
|
4436
|
+
return rows.length === 1 ? rows[0] : null;
|
|
4437
|
+
}
|
|
4341
4438
|
function animaRegionalPromptRefusal(type, widget) {
|
|
4342
4439
|
const wired = ANIMA_REGIONAL_WIRED_PROMPT_INPUT[widget];
|
|
4343
4440
|
const route = wired
|
|
@@ -4367,6 +4464,162 @@ async function refuseAnimaRegionalPromptWrite(ctx, nodeId, widget) {
|
|
|
4367
4464
|
return null;
|
|
4368
4465
|
return animaRegionalPromptRefusal(type, widget);
|
|
4369
4466
|
}
|
|
4467
|
+
function daSiWaStackRefusal(type) {
|
|
4468
|
+
return fail(`panel_set_widget cannot set "${DASIWA_STACK_WIDGET}" on ${type}. ` +
|
|
4469
|
+
`The node's custom multi-row widget owns the LoRA stack in its internal JS state and ` +
|
|
4470
|
+
`re-serializes that state over widget.value, so the normal graph_set_widget success ` +
|
|
4471
|
+
`echo would be a false success. Edit the stack rows in the node UI instead; do not retry ` +
|
|
4472
|
+
`panel_set_widget for this widget.`);
|
|
4473
|
+
}
|
|
4474
|
+
function daSiWaIdentityRefusal(reason) {
|
|
4475
|
+
return fail(`panel_set_widget refused "${DASIWA_STACK_WIDGET}" because the panel probe could not ` +
|
|
4476
|
+
`prove the requested node is the intended ${DASIWA_LTX2_LORA_LOADER} (${reason}). ` +
|
|
4477
|
+
`No graph_set_widget was dispatched. Edit the stack rows in the node UI instead; ` +
|
|
4478
|
+
`do not retry this write until the node identity can be read exactly.`);
|
|
4479
|
+
}
|
|
4480
|
+
function isUsablePanelConnectionIdentity(identity) {
|
|
4481
|
+
return (!!identity &&
|
|
4482
|
+
Number.isSafeInteger(identity.generation) &&
|
|
4483
|
+
typeof identity.tabSessionId === "string" &&
|
|
4484
|
+
identity.tabSessionId.length > 0);
|
|
4485
|
+
}
|
|
4486
|
+
function samePanelConnectionIdentity(left, right) {
|
|
4487
|
+
return left.generation === right.generation && left.tabSessionId === right.tabSessionId;
|
|
4488
|
+
}
|
|
4489
|
+
/** Refuse the DaSiWa stack widget whose custom UI deterministically overwrites a
|
|
4490
|
+
* graph_set_widget write after acknowledging it. This gate fails closed unless
|
|
4491
|
+
* the probe proves the requested node and remains bound to the same panel tab. */
|
|
4492
|
+
async function refuseDaSiWaStackWrite(ctx, nodeId, widget) {
|
|
4493
|
+
if (widget !== DASIWA_STACK_WIDGET)
|
|
4494
|
+
return null;
|
|
4495
|
+
try {
|
|
4496
|
+
if (ctx.tabExpectedNodeTypeFenceCapability?.() !== true) {
|
|
4497
|
+
return daSiWaIdentityRefusal("the bound panel does not advertise the atomic expected-node-type write fence; update the panel and hard-refresh");
|
|
4498
|
+
}
|
|
4499
|
+
}
|
|
4500
|
+
catch {
|
|
4501
|
+
return daSiWaIdentityRefusal("the panel expected-node-type write fence could not be verified");
|
|
4502
|
+
}
|
|
4503
|
+
const tabBefore = ctx.tabId;
|
|
4504
|
+
let identityBefore;
|
|
4505
|
+
try {
|
|
4506
|
+
identityBefore = ctx.panelConnectionIdentity?.();
|
|
4507
|
+
}
|
|
4508
|
+
catch {
|
|
4509
|
+
return daSiWaIdentityRefusal("the bound browser-tab identity was unreadable");
|
|
4510
|
+
}
|
|
4511
|
+
if (!isUsablePanelConnectionIdentity(identityBefore)) {
|
|
4512
|
+
return daSiWaIdentityRefusal("the bound browser-tab identity was unavailable");
|
|
4513
|
+
}
|
|
4514
|
+
let probe;
|
|
4515
|
+
try {
|
|
4516
|
+
probe = await ctx.call({
|
|
4517
|
+
cmd: "graph_query",
|
|
4518
|
+
ids: [nodeId],
|
|
4519
|
+
fields: "compact",
|
|
4520
|
+
limit: 1,
|
|
4521
|
+
});
|
|
4522
|
+
}
|
|
4523
|
+
catch {
|
|
4524
|
+
return daSiWaIdentityRefusal("the graph_query probe failed before identity could be verified");
|
|
4525
|
+
}
|
|
4526
|
+
if (ctx.tabId !== tabBefore) {
|
|
4527
|
+
return daSiWaIdentityRefusal("the probe was answered by a different panel tab");
|
|
4528
|
+
}
|
|
4529
|
+
let identityAfter;
|
|
4530
|
+
try {
|
|
4531
|
+
identityAfter = ctx.panelConnectionIdentity?.();
|
|
4532
|
+
}
|
|
4533
|
+
catch {
|
|
4534
|
+
return daSiWaIdentityRefusal("the panel-tab identity became unreadable after the probe");
|
|
4535
|
+
}
|
|
4536
|
+
if (!isUsablePanelConnectionIdentity(identityAfter)) {
|
|
4537
|
+
return daSiWaIdentityRefusal("the panel-tab identity became unavailable after the probe");
|
|
4538
|
+
}
|
|
4539
|
+
if (!samePanelConnectionIdentity(identityBefore, identityAfter)) {
|
|
4540
|
+
return daSiWaIdentityRefusal("the panel-tab connection changed during the probe");
|
|
4541
|
+
}
|
|
4542
|
+
if (probe.isError) {
|
|
4543
|
+
return daSiWaIdentityRefusal("the graph_query probe returned an error");
|
|
4544
|
+
}
|
|
4545
|
+
const identity = parseVerifiedQueriedNodeIdentity(parseToolResultJson(probe));
|
|
4546
|
+
const requestedId = canonicalQueriedNodeId(nodeId);
|
|
4547
|
+
if (!identity || !requestedId) {
|
|
4548
|
+
return daSiWaIdentityRefusal("the graph_query reply was malformed, truncated, or did not contain exactly one verifiable row");
|
|
4549
|
+
}
|
|
4550
|
+
if (identity.id !== requestedId) {
|
|
4551
|
+
return daSiWaIdentityRefusal("the graph_query row identified a different node_id");
|
|
4552
|
+
}
|
|
4553
|
+
if (identity.type !== DASIWA_LTX2_LORA_LOADER)
|
|
4554
|
+
return null;
|
|
4555
|
+
return daSiWaStackRefusal(identity.type);
|
|
4556
|
+
}
|
|
4557
|
+
/** Recheck the target immediately before a stack_data mutation. The first probe
|
|
4558
|
+
* prevents the known refusal from dispatching; this final fence catches a tab
|
|
4559
|
+
* reconnect or node replacement observed between that probe and graph_set_widget. */
|
|
4560
|
+
async function verifyDaSiWaStackWriteFence(ctx, nodeId) {
|
|
4561
|
+
try {
|
|
4562
|
+
if (ctx.tabExpectedNodeTypeFenceCapability?.() !== true) {
|
|
4563
|
+
return daSiWaIdentityRefusal("the bound panel does not advertise the atomic expected-node-type write fence; update the panel and hard-refresh");
|
|
4564
|
+
}
|
|
4565
|
+
}
|
|
4566
|
+
catch {
|
|
4567
|
+
return daSiWaIdentityRefusal("the panel expected-node-type write fence could not be verified");
|
|
4568
|
+
}
|
|
4569
|
+
const tabBefore = ctx.tabId;
|
|
4570
|
+
let identityBefore;
|
|
4571
|
+
try {
|
|
4572
|
+
identityBefore = ctx.panelConnectionIdentity?.();
|
|
4573
|
+
}
|
|
4574
|
+
catch {
|
|
4575
|
+
return daSiWaIdentityRefusal("the bound browser-tab identity was unreadable before dispatch");
|
|
4576
|
+
}
|
|
4577
|
+
if (!isUsablePanelConnectionIdentity(identityBefore)) {
|
|
4578
|
+
return daSiWaIdentityRefusal("the bound browser-tab identity was unavailable before dispatch");
|
|
4579
|
+
}
|
|
4580
|
+
let probe;
|
|
4581
|
+
try {
|
|
4582
|
+
probe = await ctx.call({
|
|
4583
|
+
cmd: "graph_query",
|
|
4584
|
+
ids: [nodeId],
|
|
4585
|
+
fields: "compact",
|
|
4586
|
+
limit: 1,
|
|
4587
|
+
});
|
|
4588
|
+
}
|
|
4589
|
+
catch {
|
|
4590
|
+
return daSiWaIdentityRefusal("the final graph_query fence failed before dispatch");
|
|
4591
|
+
}
|
|
4592
|
+
if (ctx.tabId !== tabBefore) {
|
|
4593
|
+
return daSiWaIdentityRefusal("the final fence was answered by a different panel tab");
|
|
4594
|
+
}
|
|
4595
|
+
let identityAfter;
|
|
4596
|
+
try {
|
|
4597
|
+
identityAfter = ctx.panelConnectionIdentity?.();
|
|
4598
|
+
}
|
|
4599
|
+
catch {
|
|
4600
|
+
return daSiWaIdentityRefusal("the panel-tab identity became unreadable before dispatch");
|
|
4601
|
+
}
|
|
4602
|
+
if (!isUsablePanelConnectionIdentity(identityAfter)) {
|
|
4603
|
+
return daSiWaIdentityRefusal("the panel-tab identity became unavailable before dispatch");
|
|
4604
|
+
}
|
|
4605
|
+
if (!samePanelConnectionIdentity(identityBefore, identityAfter)) {
|
|
4606
|
+
return daSiWaIdentityRefusal("the panel-tab connection changed before dispatch");
|
|
4607
|
+
}
|
|
4608
|
+
if (probe.isError) {
|
|
4609
|
+
return daSiWaIdentityRefusal("the final graph_query fence returned an error");
|
|
4610
|
+
}
|
|
4611
|
+
const identity = parseVerifiedQueriedNodeIdentity(parseToolResultJson(probe));
|
|
4612
|
+
const requestedId = canonicalQueriedNodeId(nodeId);
|
|
4613
|
+
if (!identity || !requestedId) {
|
|
4614
|
+
return daSiWaIdentityRefusal("the final graph_query fence was malformed, truncated, or did not contain exactly one verifiable row");
|
|
4615
|
+
}
|
|
4616
|
+
if (identity.id !== requestedId) {
|
|
4617
|
+
return daSiWaIdentityRefusal("the final graph_query fence identified a different node_id");
|
|
4618
|
+
}
|
|
4619
|
+
return identity.type === DASIWA_LTX2_LORA_LOADER
|
|
4620
|
+
? daSiWaStackRefusal(identity.type)
|
|
4621
|
+
: { expectedNodeType: identity.type };
|
|
4622
|
+
}
|
|
4370
4623
|
// ---- #809: turn the panel's silent `truncated: true` booleans into a remedy --------
|
|
4371
4624
|
//
|
|
4372
4625
|
// A bare boolean is the WORST truncation signal there is: it is a field, not prose, so
|
|
@@ -10216,6 +10469,7 @@ export function makePanelToolCtx(bridge, tabId, workflowTargets, onRunTicketOpen
|
|
|
10216
10469
|
ctx.panelConnectionIdentity = panelConnectionIdentity;
|
|
10217
10470
|
ctx.awaitPostRestartReachable = awaitPostRestartReachable;
|
|
10218
10471
|
ctx.tabCanMutateGraph = () => bridge.tabCanMutateGraph(ctx.tabId);
|
|
10472
|
+
ctx.tabExpectedNodeTypeFenceCapability = () => bridge.tabExpectedNodeTypeFenceCapability(ctx.tabId);
|
|
10219
10473
|
ctx.tabGraphMutationCapability = () => bridge.tabGraphMutationCapability(ctx.tabId);
|
|
10220
10474
|
return ctx;
|
|
10221
10475
|
}
|
|
@@ -12386,7 +12640,7 @@ export function buildPanelToolDefs() {
|
|
|
12386
12640
|
node_id: nodeId().describe("Node id whose input to disconnect."),
|
|
12387
12641
|
input: slotRef.optional().describe("Input slot name or index (default 0)."),
|
|
12388
12642
|
}, async (args, ctx) => ctx.call({ cmd: "graph_disconnect", node_id: args.node_id, input: args.input })),
|
|
12389
|
-
def("panel_set_widget", "Set a widget value on a node in the user's open graph (steps, cfg, seed, ckpt_name, text prompts, …). Returns the previous and new value (a string longer than 1000 chars is echoed as {chars, sha256, preview} so batched calls stay inside the outer tool-result budget — pass `echo: \"full\"` for the verbatim string). Undoable with Ctrl+Z. To CLEAR a text widget to an empty string, pass `clear: true` (some MCP clients drop an empty-string `value` from the serialized payload, so `value: \"\"` may not arrive — `clear: true` always works). For the LTXDirector timeline node (WhatDreamsCost CSGlide), set `timeline_data` with the FULL timeline JSON (segments + global_prompt) to drive its custom timeline UI — this re-syncs the editor and regenerates its derived `local_prompts`/`segment_lengths`/`guide_strength` widgets; setting those derived widgets directly is refused (they are silently reverted). For AnimaRegionalCanvasInline / Krea2RegionalCanvasInline (LC123), quality/scene/red/green/blue/negative prompt writes are refused: the custom textarea and node.properties.animaPrompts overwrite widget.value on APPLY. Drive quality/scene/negative via a PrimitiveStringMultiline wired into quality_prompt_in / scene_prompt_in / negative_prompt_in; red/green/blue have no socket.", {
|
|
12643
|
+
def("panel_set_widget", "Set a widget value on a node in the user's open graph (steps, cfg, seed, ckpt_name, text prompts, …). Returns the previous and new value (a string longer than 1000 chars is echoed as {chars, sha256, preview} so batched calls stay inside the outer tool-result budget — pass `echo: \"full\"` for the verbatim string). Undoable with Ctrl+Z. To CLEAR a text widget to an empty string, pass `clear: true` (some MCP clients drop an empty-string `value` from the serialized payload, so `value: \"\"` may not arrive — `clear: true` always works). For the LTXDirector timeline node (WhatDreamsCost CSGlide), set `timeline_data` with the FULL timeline JSON (segments + global_prompt) to drive its custom timeline UI — this re-syncs the editor and regenerates its derived `local_prompts`/`segment_lengths`/`guide_strength` widgets; setting those derived widgets directly is refused (they are silently reverted). For AnimaRegionalCanvasInline / Krea2RegionalCanvasInline (LC123), quality/scene/red/green/blue/negative prompt writes are refused: the custom textarea and node.properties.animaPrompts overwrite widget.value on APPLY. Drive quality/scene/negative via a PrimitiveStringMultiline wired into quality_prompt_in / scene_prompt_in / negative_prompt_in; red/green/blue have no socket. DaSiWa_LTX2LoraLoader's `stack_data` write is also refused: its custom multi-row widget reserializes its own JS state over `widget.value`, so the echoed write would be a false success; edit the stack rows in the node UI instead.", {
|
|
12390
12644
|
node_id: nodeId().describe("Node id from panel_graph_outline / panel_query_graph."),
|
|
12391
12645
|
widget: z.string().describe("Widget name (e.g. 'steps', 'cfg', 'text')."),
|
|
12392
12646
|
value: z
|
|
@@ -12417,6 +12671,16 @@ export function buildPanelToolDefs() {
|
|
|
12417
12671
|
const blocked = await refuseAnimaRegionalPromptWrite(ctx, args.node_id, args.widget);
|
|
12418
12672
|
if (blocked)
|
|
12419
12673
|
return blocked;
|
|
12674
|
+
const daSiWaBlocked = await refuseDaSiWaStackWrite(ctx, args.node_id, args.widget);
|
|
12675
|
+
if (daSiWaBlocked)
|
|
12676
|
+
return daSiWaBlocked;
|
|
12677
|
+
let expectedNodeType;
|
|
12678
|
+
if (args.widget === DASIWA_STACK_WIDGET) {
|
|
12679
|
+
const daSiWaFence = await verifyDaSiWaStackWriteFence(ctx, args.node_id);
|
|
12680
|
+
if (daSiWaFence && "content" in daSiWaFence)
|
|
12681
|
+
return daSiWaFence;
|
|
12682
|
+
expectedNodeType = daSiWaFence?.expectedNodeType;
|
|
12683
|
+
}
|
|
12420
12684
|
// #599: the frontend runs refresh-before-validate here (pulls a fresh
|
|
12421
12685
|
// /object_info so a just-staged/-downloaded/-installed value is accepted on
|
|
12422
12686
|
// a single revalidation, #338/#458) — that authoritative fetch can outlast
|
|
@@ -12429,7 +12693,19 @@ export function buildPanelToolDefs() {
|
|
|
12429
12693
|
// to the raw success reply so a later appendToolResultText disclosure
|
|
12430
12694
|
// (which makes the text no longer parse as JSON) cannot dodge it.
|
|
12431
12695
|
const echoFull = args.echo === "full";
|
|
12432
|
-
const write = async (nodeId, widget) => stripVerifiedLastObservedSchemaNote(summarizeSetWidgetEcho(await ctx.call({
|
|
12696
|
+
const write = async (nodeId, widget, targetExpectedNodeType = expectedNodeType) => stripVerifiedLastObservedSchemaNote(summarizeSetWidgetEcho(await ctx.call({
|
|
12697
|
+
cmd: "graph_set_widget",
|
|
12698
|
+
node_id: nodeId,
|
|
12699
|
+
widget,
|
|
12700
|
+
value,
|
|
12701
|
+
// The caller supplies the type proven for THIS addressed node.
|
|
12702
|
+
// The outer final fence is used for direct/re-mapped writes;
|
|
12703
|
+
// promoted recovery re-probes its inner node after entering
|
|
12704
|
+
// and supplies that node's own type.
|
|
12705
|
+
...(targetExpectedNodeType
|
|
12706
|
+
? { expected_node_type: targetExpectedNodeType }
|
|
12707
|
+
: {}),
|
|
12708
|
+
}, OBJECT_INFO_REFRESH_ACK_TIMEOUT_MS), echoFull));
|
|
12433
12709
|
let first = await write(args.node_id, args.widget);
|
|
12434
12710
|
if (!first.isError) {
|
|
12435
12711
|
markPanelSchemaReady(panelSchemaKey(ctx));
|
|
@@ -12527,7 +12803,38 @@ export function buildPanelToolDefs() {
|
|
|
12527
12803
|
`Resolved it to inner node ${inner.innerNodeId} but panel_enter_subgraph FAILED: ` +
|
|
12528
12804
|
`${textOfToolResult(entered)})`);
|
|
12529
12805
|
}
|
|
12530
|
-
|
|
12806
|
+
let innerExpectedNodeType;
|
|
12807
|
+
if (expectedNodeType !== undefined) {
|
|
12808
|
+
// The inner mapping was discovered before an awaited enter. Re-query
|
|
12809
|
+
// the addressed inner node after entering so the stack_data write gets
|
|
12810
|
+
// its OWN live type fence; carrying the outer wrapper's type would
|
|
12811
|
+
// reject a valid inner node, while omitting a fence would let a same-
|
|
12812
|
+
// type replacement mutate a detached object and report false success.
|
|
12813
|
+
const innerProbe = await ctx.call({ cmd: "graph_query", ids: [inner.innerNodeId], fields: "compact", limit: 1 }, OBJECT_INFO_REFRESH_ACK_TIMEOUT_MS);
|
|
12814
|
+
const innerIdentity = innerProbe.isError
|
|
12815
|
+
? null
|
|
12816
|
+
: parseVerifiedQueriedNodeIdentity(parseToolResultJson(innerProbe));
|
|
12817
|
+
const expectedInnerId = canonicalQueriedNodeId(inner.innerNodeId);
|
|
12818
|
+
if (!innerIdentity || !expectedInnerId || innerIdentity.id !== expectedInnerId) {
|
|
12819
|
+
const exited = await ctx.call({ cmd: "graph_exit_subgraph" }, 15000);
|
|
12820
|
+
return appendToolResultText(first, `\n\n(The panel listed "${refusal.widget}" as promoted and mapped it to inner node ` +
|
|
12821
|
+
`${inner.innerNodeId}, but the post-enter identity probe did not verify that ` +
|
|
12822
|
+
`exact live node${innerProbe.isError ? `: ${textOfToolResult(innerProbe)}` : "."} ` +
|
|
12823
|
+
`The write was not retried.${exited.isError
|
|
12824
|
+
? ` panel_exit_subgraph also FAILED: ${textOfToolResult(exited)}`
|
|
12825
|
+
: ""})`);
|
|
12826
|
+
}
|
|
12827
|
+
if (innerIdentity.type === DASIWA_LTX2_LORA_LOADER) {
|
|
12828
|
+
const exited = await ctx.call({ cmd: "graph_exit_subgraph" }, 15000);
|
|
12829
|
+
return appendToolResultText(first, `\n\n(The promoted inner node ${inner.innerNodeId} was identified as ` +
|
|
12830
|
+
`${DASIWA_LTX2_LORA_LOADER}; ${textOfToolResult(daSiWaStackRefusal(innerIdentity.type))} ` +
|
|
12831
|
+
`No inner graph_set_widget was dispatched.${exited.isError
|
|
12832
|
+
? ` panel_exit_subgraph also FAILED: ${textOfToolResult(exited)}`
|
|
12833
|
+
: ""})`);
|
|
12834
|
+
}
|
|
12835
|
+
innerExpectedNodeType = innerIdentity.type;
|
|
12836
|
+
}
|
|
12837
|
+
const written = await write(inner.innerNodeId, inner.widget, innerExpectedNodeType);
|
|
12531
12838
|
const exited = await ctx.call({ cmd: "graph_exit_subgraph" }, 15000);
|
|
12532
12839
|
if (!written.isError) {
|
|
12533
12840
|
const via = `\n\n(Applied via the inner widget this promotion lists: node ${inner.innerNodeId} ` +
|