fix: preserve a live title, flags and colour when serializing missing-node placeholders (BE-8848) - #15719
Conversation
…-node placeholders serialize() replays last_serialization for a missing-node placeholder and previously overrode only mode, pos and size, so a rename, recolour, collapse or pin on a placeholder was discarded on reload. configure() copies the file title/flags/color/bgcolor onto the live node at load, so live-wins on these four fields can only ever carry a deliberate user edit. Unset decorations are deleted rather than conditionally spread, so clearing a colour cannot resurrect the one the file recorded, and the flags key is only emitted when the file had one or the live node has something to say, which keeps an untouched round-trip byte-identical.
|
Important Approval pendingCodeRabbit has no unresolved comments, but it has not reviewed the latest commit. Use the checkbox below to review the latest commit. CodeRabbit will approve the changes if it finds no blocking issues.
Comment |
🎨 Storybook: ✅ Built — View Storybook🎭 Playwright: ✅ 1983 passed, 0 failed · 1 flaky📊 Browser Reports
📦 Bundle Size
⚡ Performance Report
Absolute values
Raw data{
"timestamp": "2026-08-23T09:12:22.967Z",
"gitSha": "900be043af14df76d97b4219881ef855a066dac4",
"branch": "matt/be-8848-placeholder-decorations",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2107.8239999999937,
"styleRecalcs": 6,
"styleRecalcDurationMs": 6.058999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 644.0010000000001,
"heapDeltaBytes": 20032064,
"heapUsedBytes": 81708312,
"domNodes": -286,
"jsHeapTotalBytes": 4448256,
"scriptDurationMs": 10.413000000000002,
"eventListeners": -153,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-idle",
"durationMs": 2053.9209999999457,
"styleRecalcs": 9,
"styleRecalcDurationMs": 9.011999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 545.49,
"heapDeltaBytes": 17192312,
"heapUsedBytes": 79590352,
"domNodes": -282,
"jsHeapTotalBytes": 4710400,
"scriptDurationMs": 9.681999999999999,
"eventListeners": -151,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1962.9620000000045,
"styleRecalcs": 75,
"styleRecalcDurationMs": 42.135,
"layouts": 12,
"layoutDurationMs": 3.701,
"taskDurationMs": 942.4330000000001,
"heapDeltaBytes": 15931648,
"heapUsedBytes": 78241780,
"domNodes": -281,
"jsHeapTotalBytes": 5496832,
"scriptDurationMs": 115.267,
"eventListeners": -151,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1953.538999999978,
"styleRecalcs": 76,
"styleRecalcDurationMs": 42.729,
"layouts": 12,
"layoutDurationMs": 3.7820000000000005,
"taskDurationMs": 932.279,
"heapDeltaBytes": 15837744,
"heapUsedBytes": 78248628,
"domNodes": -281,
"jsHeapTotalBytes": 5234688,
"scriptDurationMs": 116.02100000000002,
"eventListeners": -151,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1713.2689999999684,
"styleRecalcs": 31,
"styleRecalcDurationMs": 17.315,
"layouts": 6,
"layoutDurationMs": 0.7599999999999998,
"taskDurationMs": 388.509,
"heapDeltaBytes": 3100072,
"heapUsedBytes": 64702416,
"domNodes": 76,
"jsHeapTotalBytes": 5242880,
"scriptDurationMs": 12.297,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1737.1090000000322,
"styleRecalcs": 32,
"styleRecalcDurationMs": 18.993,
"layouts": 6,
"layoutDurationMs": 0.6759999999999999,
"taskDurationMs": 404.566,
"heapDeltaBytes": 3016288,
"heapUsedBytes": 65075860,
"domNodes": 78,
"jsHeapTotalBytes": 5242880,
"scriptDurationMs": 12.073,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "dom-widget-clipping",
"durationMs": 583.0829999999878,
"styleRecalcs": 11,
"styleRecalcDurationMs": 7.3919999999999995,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 366.05199999999996,
"heapDeltaBytes": 10928608,
"heapUsedBytes": 73247940,
"domNodes": 18,
"jsHeapTotalBytes": 5242880,
"scriptDurationMs": 57.66,
"eventListeners": 0,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "dom-widget-clipping",
"durationMs": 586.4039999999022,
"styleRecalcs": 11,
"styleRecalcDurationMs": 7.955,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 378.799,
"heapDeltaBytes": 10833544,
"heapUsedBytes": 73239392,
"domNodes": 18,
"jsHeapTotalBytes": 5505024,
"scriptDurationMs": 61.163,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-idle",
"durationMs": 2055.3170000000023,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.944,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 696.7570000000001,
"heapDeltaBytes": -13765064,
"heapUsedBytes": 61927412,
"domNodes": -281,
"jsHeapTotalBytes": 2879488,
"scriptDurationMs": 16.385000000000005,
"eventListeners": -181,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "large-graph-idle",
"durationMs": 2039.0879999999925,
"styleRecalcs": 8,
"styleRecalcDurationMs": 7.408000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 659.009,
"heapDeltaBytes": -13038252,
"heapUsedBytes": 63737004,
"domNodes": -281,
"jsHeapTotalBytes": 3403776,
"scriptDurationMs": 17.862999999999996,
"eventListeners": -181,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "large-graph-pan",
"durationMs": 2347.46100000001,
"styleRecalcs": 70,
"styleRecalcDurationMs": 15.867000000000003,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1286.1250000000002,
"heapDeltaBytes": -5359692,
"heapUsedBytes": 72244624,
"domNodes": -273,
"jsHeapTotalBytes": 483328,
"scriptDurationMs": 338.34299999999996,
"eventListeners": -151,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "large-graph-pan",
"durationMs": 2454.4329999999945,
"styleRecalcs": 68,
"styleRecalcDurationMs": 13.701,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1315.115,
"heapDeltaBytes": -5041540,
"heapUsedBytes": 72698008,
"domNodes": -275,
"jsHeapTotalBytes": -1875968,
"scriptDurationMs": 365.94599999999997,
"eventListeners": -149,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "large-graph-zoom",
"durationMs": 3314.130999999975,
"styleRecalcs": 65,
"styleRecalcDurationMs": 15.403,
"layouts": 60,
"layoutDurationMs": 7.739999999999999,
"taskDurationMs": 1475.249,
"heapDeltaBytes": -4023316,
"heapUsedBytes": 73673204,
"domNodes": -286,
"jsHeapTotalBytes": 4976640,
"scriptDurationMs": 400.241,
"eventListeners": -185,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3264.33099999997,
"styleRecalcs": 65,
"styleRecalcDurationMs": 15.404000000000002,
"layouts": 60,
"layoutDurationMs": 7.960999999999999,
"taskDurationMs": 1451.52,
"heapDeltaBytes": -7174588,
"heapUsedBytes": 71073732,
"domNodes": -287,
"jsHeapTotalBytes": 4976640,
"scriptDurationMs": 402.74199999999996,
"eventListeners": -181,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "legacy-node-drag",
"durationMs": 2479.575000000011,
"styleRecalcs": 46,
"styleRecalcDurationMs": 10.639999999999997,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1470.8239999999998,
"heapDeltaBytes": -14937088,
"heapUsedBytes": 69987100,
"domNodes": 12,
"jsHeapTotalBytes": 8163328,
"scriptDurationMs": 470.639,
"eventListeners": 186,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "legacy-node-drag",
"durationMs": 2556.1609999999746,
"styleRecalcs": 45,
"styleRecalcDurationMs": 10.293000000000003,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1577.9750000000001,
"heapDeltaBytes": -20541804,
"heapUsedBytes": 64294308,
"domNodes": -247,
"jsHeapTotalBytes": 8122368,
"scriptDurationMs": 490.721,
"eventListeners": 31,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "minimap-idle",
"durationMs": 2042.8009999999972,
"styleRecalcs": 8,
"styleRecalcDurationMs": 8.300000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 700.2120000000001,
"heapDeltaBytes": -12676564,
"heapUsedBytes": 70298712,
"domNodes": -280,
"jsHeapTotalBytes": 4190208,
"scriptDurationMs": 16.732,
"eventListeners": -151,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "minimap-idle",
"durationMs": 2045.2420000000302,
"styleRecalcs": 8,
"styleRecalcDurationMs": 8.302,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 697.9530000000001,
"heapDeltaBytes": -8699988,
"heapUsedBytes": 74301820,
"domNodes": -271,
"jsHeapTotalBytes": 4976640,
"scriptDurationMs": 18.132,
"eventListeners": -151,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 596.2600000000293,
"styleRecalcs": 46,
"styleRecalcDurationMs": 10.912,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 400.96,
"heapDeltaBytes": 11709476,
"heapUsedBytes": 74437944,
"domNodes": 18,
"jsHeapTotalBytes": 5242880,
"scriptDurationMs": 117.137,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 589.6389999999201,
"styleRecalcs": 46,
"styleRecalcDurationMs": 10,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 399.938,
"heapDeltaBytes": 11476160,
"heapUsedBytes": 74041420,
"domNodes": 18,
"jsHeapTotalBytes": 5505024,
"scriptDurationMs": 116.15,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-idle",
"durationMs": 2017.9469999999924,
"styleRecalcs": 8,
"styleRecalcDurationMs": 7.821999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 547.134,
"heapDeltaBytes": 1332172,
"heapUsedBytes": 63674472,
"domNodes": -283,
"jsHeapTotalBytes": 4448256,
"scriptDurationMs": 8.850999999999999,
"eventListeners": -181,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-idle",
"durationMs": 2055.024000000003,
"styleRecalcs": 8,
"styleRecalcDurationMs": 8.394,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 562.42,
"heapDeltaBytes": 3793216,
"heapUsedBytes": 66476440,
"domNodes": -282,
"jsHeapTotalBytes": 4448256,
"scriptDurationMs": 9.715000000000002,
"eventListeners": -181,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1717.6860000000147,
"styleRecalcs": 75,
"styleRecalcDurationMs": 34.894999999999996,
"layouts": 16,
"layoutDurationMs": 3.927999999999999,
"taskDurationMs": 821.712,
"heapDeltaBytes": -5919416,
"heapUsedBytes": 56192632,
"domNodes": -280,
"jsHeapTotalBytes": 5496832,
"scriptDurationMs": 86.699,
"eventListeners": -151,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1707.1559999999408,
"styleRecalcs": 75,
"styleRecalcDurationMs": 37.606,
"layouts": 16,
"layoutDurationMs": 4.784999999999999,
"taskDurationMs": 847.2109999999999,
"heapDeltaBytes": 16995180,
"heapUsedBytes": 79697816,
"domNodes": -280,
"jsHeapTotalBytes": 4186112,
"scriptDurationMs": 95.249,
"eventListeners": -151,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "subgraph-transition-enter",
"durationMs": 1402.1549999999934,
"styleRecalcs": 19,
"styleRecalcDurationMs": 32.074000000000005,
"layouts": 14,
"layoutDurationMs": 14.977,
"taskDurationMs": 994.363,
"heapDeltaBytes": -6078124,
"heapUsedBytes": 87591172,
"domNodes": 13673,
"jsHeapTotalBytes": 12582912,
"scriptDurationMs": 18.334999999999997,
"eventListeners": 2373,
"totalBlockingTimeMs": 143,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8515.938000000006,
"styleRecalcs": 250,
"styleRecalcDurationMs": 39.313,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4439.2300000000005,
"heapDeltaBytes": -2225492,
"heapUsedBytes": 74627404,
"domNodes": -274,
"jsHeapTotalBytes": 483328,
"scriptDurationMs": 1072.394,
"eventListeners": -135,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8729.765000000043,
"styleRecalcs": 251,
"styleRecalcDurationMs": 44.782,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4561.591,
"heapDeltaBytes": -6414516,
"heapUsedBytes": 70332368,
"domNodes": -279,
"jsHeapTotalBytes": -827392,
"scriptDurationMs": 1078.13,
"eventListeners": -133,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 18174.65100000004,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 17153.891000000003,
"heapDeltaBytes": -34132488,
"heapUsedBytes": 177993004,
"domNodes": -8312,
"jsHeapTotalBytes": -17608704,
"scriptDurationMs": 122.44400000000002,
"eventListeners": -16391,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.779999999999927,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 18270.99499999997,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 17371.301,
"heapDeltaBytes": -49069680,
"heapUsedBytes": 180948752,
"domNodes": -8312,
"jsHeapTotalBytes": -16588800,
"scriptDurationMs": 121.43500000000002,
"eventListeners": -16389,
"totalBlockingTimeMs": 0,
"frameDurationMs": 18.333333333333332,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "vue-large-graph-pan",
"durationMs": 21301.301999999963,
"styleRecalcs": 165,
"styleRecalcDurationMs": 20.24499999999996,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 20810.484,
"heapDeltaBytes": -40208196,
"heapUsedBytes": 186653772,
"domNodes": -8312,
"jsHeapTotalBytes": -17776640,
"scriptDurationMs": 423.05800000000005,
"eventListeners": -16385,
"totalBlockingTimeMs": 189,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-pan",
"durationMs": 21466.365999999995,
"styleRecalcs": 171,
"styleRecalcDurationMs": 20.538,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 20894.197,
"heapDeltaBytes": -44971448,
"heapUsedBytes": 173118976,
"domNodes": -8312,
"jsHeapTotalBytes": -23678976,
"scriptDurationMs": 479.555,
"eventListeners": -16387,
"totalBlockingTimeMs": 112,
"frameDurationMs": 17.780000000000047,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "workflow-execution",
"durationMs": 115.33399999996163,
"styleRecalcs": 7,
"styleRecalcDurationMs": 15.205,
"layouts": 2,
"layoutDurationMs": 1.272,
"taskDurationMs": 83.74300000000001,
"heapDeltaBytes": 3107076,
"heapUsedBytes": 65551300,
"domNodes": 129,
"jsHeapTotalBytes": 262144,
"scriptDurationMs": 7.353999999999999,
"eventListeners": 25,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "workflow-execution",
"durationMs": 113.5469999999259,
"styleRecalcs": 7,
"styleRecalcDurationMs": 14.134,
"layouts": 3,
"layoutDurationMs": 0.6119999999999999,
"taskDurationMs": 76.472,
"heapDeltaBytes": 3106912,
"heapUsedBytes": 64554876,
"domNodes": 110,
"jsHeapTotalBytes": 0,
"scriptDurationMs": 5.966000000000001,
"eventListeners": 49,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
}
]
} |
There was a problem hiding this comment.
🔍 Cursor Review — Consolidated panel
Triggered by @mattmillerai.
Found 4 finding(s).
| Severity | Count |
|---|---|
| 🟡 Medium | 2 |
| 🟢 Low | 2 |
Panel: 8/8 reviewers contributed findings.
Codecov Report❌ Patch coverage is
@@ Coverage Diff @@
## matt/fe-1817-serialize-placeholder-size #15719 +/- ##
===========================================================================
- Coverage 81.90% 81.89% -0.02%
===========================================================================
Files 1888 1888
Lines 107284 107287 +3
Branches 29617 30087 +470
===========================================================================
- Hits 87869 87858 -11
- Misses 19077 19093 +16
+ Partials 338 336 -2
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 10 files with indirect coverage changes 🚀 New features to boost your workflow:
|
Review follow-up on the placeholder decoration mirror.
shape and boxcolor are serialized by the normal path and are user-mutable on
a placeholder (useNodeCustomization.applyShape and LGraphCanvas
.onMenuNodeShapes act on any selected LGraphNode), so leaving them to replay
from the file dropped those edits on save - the same data loss this branch
sets out to fix. They now join title, color and bgcolor in the live mirror.
The five mirrored keys are omitted from the replayed serialization before the
live values are spread back over it, which replaces the per-key delete chain
and keeps "cleared decorations do not resurrect the recorded value" true for
every key by construction rather than one branch at a time.
flags is required by zComfyNode and zFlags is a non-nullable object, so the
previous absence round-trip emitted a node the workflow schema rejects, which
surfaces as a validation alert and skips tryFixLinks repair on the next open.
An empty object faithfully represents "no flags" and is schema-valid, so the
key is now always emitted; the v0.4 fixture snapshot gains "flags": {}
accordingly. Guarding this.flags with optional chaining also keeps the branch
consistent with the defensive reads elsewhere in the class.
|
🤖 The reviews loop filed Linear follow-up ticket(s) for review thread(s) deferred as out of scope for this PR:
The following carry
|
STACKED — merging lands on
matt/fe-1817-serialize-placeholder-size(owned by @mattmillerai, PR #15713), NOTmain. Review/merge #15713 first; this PR's diff is only the commit on top.ELI-5
If ComfyUI can't find a node's code, it draws a grey stand-in and remembers the node's original text so nothing is lost when you install the missing pack later. You could rename that stand-in, colour it, reshape it, collapse it or pin it — and every one of those edits vanished the next time you opened the workflow, because saving replayed the remembered text verbatim. Now your edits stick, and a stand-in you never touched still saves what it loaded.
Summary
serialize()'s missing-node placeholder branch replayslast_serializationand overrides onlymode/pos(and, on the branch this stacks on,size). This extends that override to every user-decoration field —title,flags,color,bgcolor,boxcolor,shape— so a rename, recolour, reshape, collapse or pin on a placeholder survives save + reload.Changes
LGraphNode.serialize(), the live node now wins fortitle,flags,color,bgcolor,boxcolorandshape.configure()copies those from the file onto the live node at load, so they can only diverge from the file when the user deliberately edited them — live-wins can never lose information the file had.omitted from the replayed serialization and the live values spread back over it, so "a cleared decoration cannot resurrect the file's recorded value" holds for every key by construction instead of onedeletebranch at a time.titlereuses the normal path'sthis.title !== this.constructor.titleguard so a file with no title does not gain one on round-trip.flagsis always emitted.zComfyNode.flagsis required andzFlagsis a non-nullable object, so round-tripping the absence offlagsproduced a node the workflow schema rejects — a validation alert, andtryFixLinksrepair skipped on the next open.{}faithfully represents "no flags" and is schema-valid.o.flagsiscloneObject(this.flags), sopinnedandcollapsedride along and the replayed object no longer aliaseslast_serialization.flags.type,inputs,outputs,properties,widgets_values,widgets_values_namedstill replay from the file — no node definition exists to regenerate them. Thewidgets_valuesindex asymmetry is separately tracked (Widget values shift by one slot on reload when an earlier widget has serialize === false #15669 / PR fix: restore widgets_values by full widget index (#15669) #15688) and is not bundled here.flagskey now round-trips as"flags": {}— deliberate, and the reasonLGraph.test.ts.snap's v0.4 fixture gains that key.Review Focus
Chesterton's-fence check on
flags. This flips a field that previously always replayed from the file, so I read the history first.bdf6d4dea2("Allow updating position and mode on missing nodes", #6792, AustinMroz) introduced the replay to keep the "last known good" serialization so the node still works once the missing pack is installed, and addedpos/modeas the "small and safe changes" that should not be discarded. The decoration fields are the same class — pure decoration that cannot affect whether the node resolves when the pack is reinstalled — so extending the same override is consistent with that intent rather than against it.The
flags-absence round-trip was dropped after review. An earlier revision emittedflagsonly when the file had one or the live node had a flag to record, to keep the v0.4 fixture snapshot byte-identical. That is the wrong trade:flagsis schema-required, so preserving the absence preserves an invalid node. The snapshot now records"flags": {}and the placeholder emits a document the schema accepts.shapeandboxcolorwere folded in after review. They were initially scoped out as a separate call about decoration-vs-geometry. They are serialized by the normal path and user-mutable on a placeholder —useNodeCustomization.applyShape(src/composables/graph/useNodeCustomization.ts:113) andLGraphCanvas.onMenuNodeShapesboth act on any selectedLGraphNodewith nohas_errorsgate — so leaving them out kept the exact data-loss class this PR closes. They are decoration by the same Chesterton's-fence argument as the rest.Known adjacent gap, not fixed here (follow-up proposed).
last_serializationis a load-time snapshot that is never refreshed, and two consumers read it directly rather than throughserialize():useNodeReplacement.replaceWithMapping(src/platform/nodeReplacement/useNodeReplacement.ts:177) appliesserialized.titlefrom it, so a renamed placeholder reverts to the file's title once the missing pack is installed; and resizing an expanded placeholder then collapsing it serializes the stale recorded size. Both need a refresh policy that interacts with #15713's deliberate collapsed-size rule, so they are tracked separately rather than bundled.Provenance
pnpm vitest run src/lib/litegraph src/platform src/scripts src/composables— 537 files, 8259 passed, 7 expected-fail, 5 skipped, 0 failed;pnpm typecheck— clean;pnpm lint— 0 errors (3 pre-existing warnings in unrelated files);pnpm format— no changes outside the two edited files.LGraph.test.ts.snapintentionally updated (v0.4 fixture gains"flags": {}); no other snapshot moved.flags-absence guard (it preserved a schema-invalid document) and the exclusion ofshape/boxcolor(they are user-mutable on placeholders, so excluding them left the bug open). No test asserts clearingshape, becauseLGraphNodePropertiesinstrumentsshapeas an own data property that shadows the prototype accessor, sonode.shape = 'default'stores the raw string instead ofundefined; that is a pre-existing repo-wide quirk of the legacy string API, and the Vue path (useNodeCustomization) only ever assignsRenderShapeenum values, which are all truthy. No screenshots — this is a serialization change with no visual surface.