@manny-est/node-red-flowpilot 0.6.0 → 0.6.1
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/CHANGELOG.md +26 -0
- package/flowpilot.js +81 -42
- package/lib/core/modes.js +15 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to FlowPilot are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.6.1] - 2026-09-02
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **Deterministic run summaries actually work now.** 0.6.0 shipped this
|
|
9
|
+
feature — the per-item ✓/✗ outcome from a Modify or Generate run,
|
|
10
|
+
built from what the WRITE tools actually reported instead of the
|
|
11
|
+
model's own retelling — but a control-flow bug in the message-
|
|
12
|
+
rendering path made it unreachable for every real run: the code path
|
|
13
|
+
that shows the deterministic summary as primary (with the model's
|
|
14
|
+
own explanation demoted underneath) never executed. Every response
|
|
15
|
+
fell through to showing the model's own prose at full weight
|
|
16
|
+
instead, exactly the narrative-accuracy gap this feature exists to
|
|
17
|
+
close. Found and fixed during this release's own go-live testing,
|
|
18
|
+
now live-verified against a real agentic write.
|
|
19
|
+
- **Installed-package awareness was silently non-functional — FlowPilot
|
|
20
|
+
now actually receives your palette.** The feature that keeps
|
|
21
|
+
Generate/Modify from proposing node types you don't have, and lets
|
|
22
|
+
FlowPilot correctly answer "is X installed?", called a Node-RED API
|
|
23
|
+
that doesn't exist on this version's plugin interface. The call was
|
|
24
|
+
a silent no-op on every single request: FlowPilot never had real
|
|
25
|
+
palette information to work with, so the model either said as much
|
|
26
|
+
or, in some cases, guessed. This wasn't a timing bug or a stale
|
|
27
|
+
cache — the underlying data source itself never worked. Now reads
|
|
28
|
+
the live node registry directly and is correct immediately, from
|
|
29
|
+
the very first request after a restart.
|
|
30
|
+
|
|
5
31
|
## [0.6.0] - 2026-09-01
|
|
6
32
|
|
|
7
33
|
Promoted from `0.6.0-beta.1` to the stable `latest` npm tag.
|
package/flowpilot.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
const http = require("http");
|
|
2
1
|
const https = require("https");
|
|
3
2
|
const path = require("path");
|
|
3
|
+
const nodeRedRegistry = require("@node-red/registry");
|
|
4
4
|
const PACKAGE_VERSION = require("./package.json").version;
|
|
5
5
|
const createStorage = require("./lib/storage");
|
|
6
6
|
const openaiProvider = require("./lib/provider-openai-compatible");
|
|
@@ -189,7 +189,47 @@ async function performUpdateCheck() {
|
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
// Found live (0.6.1), while verifying the palette-cache fix below: Node-RED
|
|
193
|
+
// loads flowpilot.js TWICE per process start, and always has — package.json
|
|
194
|
+
// declares BOTH "nodes": {"flowpilot": "flowpilot-node-entry.js"} (itself a
|
|
195
|
+
// shim added earlier this same release for a related but distinct bug — see
|
|
196
|
+
// that shim's own comment) AND "plugins": {"flowpilot": "flowpilot.html"}.
|
|
197
|
+
// @node-red/registry/lib/loader.js's loadPluginConfig derives a companion
|
|
198
|
+
// runtime file for every "plugins" entry via the SAME basename-replace
|
|
199
|
+
// pattern (file.replace(/\.[^.]+$/,".js")) that caused the earlier
|
|
200
|
+
// flowpilot.html double-load — "flowpilot.html" -> "flowpilot.js", which
|
|
201
|
+
// genuinely exists, so Node-RED loads and calls it a second time completely
|
|
202
|
+
// independently of the "nodes" entry's shim. Confirmed live: a temporary
|
|
203
|
+
// diagnostic showed flowPilotRuntime executing twice per restart, and a
|
|
204
|
+
// registered RED.events.on("nodes-started", ...) listener firing twice.
|
|
205
|
+
// Unlike the client-side html/script duplication, this one can't be closed
|
|
206
|
+
// by renaming what "plugins" points to without duplicating flowpilot.html's
|
|
207
|
+
// content under a second filename — not worth the maintenance burden for
|
|
208
|
+
// what's fundamentally the same fix in spirit. Guard here instead.
|
|
209
|
+
//
|
|
210
|
+
// A first attempt at this guard used a plain module-level `let` flag,
|
|
211
|
+
// reasoning that Node's require() cache makes a module's own top-level code
|
|
212
|
+
// run exactly once. That reasoning doesn't hold here: confirmed by reading
|
|
213
|
+
// @node-red/registry/lib/loader.js directly, the "nodes" entry is loaded via
|
|
214
|
+
// loadNodeSet, which uses a dynamic import() of a file:// URL (see its
|
|
215
|
+
// `pathToFileURL(node.file)` call), while the "plugins" entry's companion
|
|
216
|
+
// file is loaded via loadPlugin, which uses a plain require(). Node's CJS
|
|
217
|
+
// require() cache and its ESM import() module cache are not the same
|
|
218
|
+
// cache — a module reached through both paths can genuinely execute its
|
|
219
|
+
// top-level code twice, each with its OWN independent closure (own
|
|
220
|
+
// `let flowPilotRuntimeInitialized`, own everything). Confirmed live: a
|
|
221
|
+
// module-level flag did NOT stop the second invocation's independent
|
|
222
|
+
// httpAdmin route registrations from being the ones actually serving
|
|
223
|
+
// requests, while the diagnostic-carrying instance sat silent.
|
|
224
|
+
//
|
|
225
|
+
// Fix: mark completion on `global` instead. Node's global object is a
|
|
226
|
+
// true process-wide singleton — unaffected by which loader/module-cache
|
|
227
|
+
// reached this file — so it's a reliable gate regardless of how many
|
|
228
|
+
// separate module realms this file's code ends up evaluated in.
|
|
192
229
|
module.exports = function flowPilotRuntime(RED) {
|
|
230
|
+
if (global.__flowPilotRuntimeInitialized) { return; }
|
|
231
|
+
global.__flowPilotRuntimeInitialized = true;
|
|
232
|
+
|
|
193
233
|
const storage = createStorage(RED.settings.userDir);
|
|
194
234
|
|
|
195
235
|
// ---------------------------------------------------------------------
|
|
@@ -703,17 +743,42 @@ module.exports = function flowPilotRuntime(RED) {
|
|
|
703
743
|
// their node types when relevant and otherwise stick to core nodes
|
|
704
744
|
// rather than proposing types that aren't installed.
|
|
705
745
|
//
|
|
706
|
-
//
|
|
707
|
-
//
|
|
708
|
-
//
|
|
709
|
-
//
|
|
710
|
-
//
|
|
711
|
-
//
|
|
746
|
+
// Node-RED's admin GET /nodes route ultimately reads the runtime registry's
|
|
747
|
+
// getNodeList(). A previous fix tried to reach that through RED.nodes, but
|
|
748
|
+
// Node-RED 5's @node-red/registry/lib/util.js createNodeApi only copies a
|
|
749
|
+
// small allow-list onto the node-level RED API (createNode/getNode/
|
|
750
|
+
// eachNode/addCredentials/getCredentials/deleteCredentials) —
|
|
751
|
+
// getNodeList is NOT one of them. Confirmed live: RED.nodes.getNodeList is
|
|
752
|
+
// undefined in this runtime, so describeInstalledNodes() quietly returned
|
|
753
|
+
// null from its catch on every request even after flows:started had fired.
|
|
754
|
+
//
|
|
755
|
+
// Use the registry module directly instead. FlowPilot already runs inside
|
|
756
|
+
// the same Node-RED process, and @node-red/registry is the exact source
|
|
757
|
+
// runtime.nodes.getNodeList() delegates to.
|
|
758
|
+
//
|
|
759
|
+
// Found live (0.6.1): this WAS a cached snapshot, through three
|
|
760
|
+
// successive designs (a naive restart-seeded TTL timer; a
|
|
761
|
+
// "nodes-started"-gated refresh; that plus a bounded settle re-check on
|
|
762
|
+
// top), because the original data source was an HTTP loopback to this
|
|
763
|
+
// same instance's own /nodes admin route — expensive enough to be worth
|
|
764
|
+
// caching, and, on any adminAuth-enabled instance, unable to
|
|
765
|
+
// authenticate itself at all (a real bug, fixed by switching to the
|
|
766
|
+
// in-process RED.nodes.getNodeList() call below). Once the data source
|
|
767
|
+
// is a synchronous, already-in-memory function call, caching it stops
|
|
768
|
+
// paying for itself — it only adds staleness/timing bugs, three of
|
|
769
|
+
// which got live-found in a row this same afternoon (the gate never
|
|
770
|
+
// set; set but the fetch 401ing; set and fetched but still an early
|
|
771
|
+
// snapshot for a slow-registering package like uibuilder). The one
|
|
772
|
+
// thing still worth gating is genuinely knowing nothing yet: before
|
|
773
|
+
// Node-RED's own "flows:started" event (the non-deprecated alias of
|
|
774
|
+
// "nodes-started", confirmed emitted from
|
|
775
|
+
// @node-red/runtime/lib/flows/index.js right after every active flow's
|
|
776
|
+
// own start() resolves) has fired at least once,
|
|
777
|
+
// describeInstalledNodes() returns null — "don't know yet" instead of a
|
|
778
|
+
// false "confirmed absent."
|
|
712
779
|
// ---------------------------------------------------------------------
|
|
713
|
-
let
|
|
714
|
-
|
|
715
|
-
let installedNodesRefreshInFlight = false;
|
|
716
|
-
const INSTALLED_NODES_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
780
|
+
let nodesRegistryReady = false;
|
|
781
|
+
RED.events.on("flows:started", function () { nodesRegistryReady = true; });
|
|
717
782
|
|
|
718
783
|
function buildInstalledNodesContent(list) {
|
|
719
784
|
if (!Array.isArray(list)) { return null; }
|
|
@@ -752,39 +817,13 @@ module.exports = function flowPilotRuntime(RED) {
|
|
|
752
817
|
return content;
|
|
753
818
|
}
|
|
754
819
|
|
|
755
|
-
function refreshInstalledNodesCache() {
|
|
756
|
-
if (installedNodesRefreshInFlight) { return; }
|
|
757
|
-
installedNodesRefreshInFlight = true;
|
|
758
|
-
|
|
759
|
-
const root = String(RED.settings.httpAdminRoot || "/").replace(/\/+$/, "");
|
|
760
|
-
const req = http.get({
|
|
761
|
-
host: "127.0.0.1",
|
|
762
|
-
port: RED.settings.uiPort,
|
|
763
|
-
path: root + "/nodes",
|
|
764
|
-
headers: { Accept: "application/json" },
|
|
765
|
-
timeout: 5000
|
|
766
|
-
}, function (res) {
|
|
767
|
-
const chunks = [];
|
|
768
|
-
res.on("data", function (chunk) { chunks.push(chunk); });
|
|
769
|
-
res.on("end", function () {
|
|
770
|
-
installedNodesRefreshInFlight = false;
|
|
771
|
-
if (res.statusCode !== 200) { return; }
|
|
772
|
-
try {
|
|
773
|
-
const list = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
774
|
-
installedNodesCache = buildInstalledNodesContent(list);
|
|
775
|
-
installedNodesCacheAt = Date.now();
|
|
776
|
-
} catch (err) { /* leave previous cache value in place */ }
|
|
777
|
-
});
|
|
778
|
-
});
|
|
779
|
-
req.on("error", function () { installedNodesRefreshInFlight = false; });
|
|
780
|
-
req.on("timeout", function () { req.destroy(); installedNodesRefreshInFlight = false; });
|
|
781
|
-
}
|
|
782
|
-
|
|
783
820
|
function describeInstalledNodes() {
|
|
784
|
-
if (
|
|
785
|
-
|
|
821
|
+
if (!nodesRegistryReady) { return null; }
|
|
822
|
+
try {
|
|
823
|
+
return buildInstalledNodesContent(nodeRedRegistry.getNodeList());
|
|
824
|
+
} catch (err) {
|
|
825
|
+
return null;
|
|
786
826
|
}
|
|
787
|
-
return installedNodesCache;
|
|
788
827
|
}
|
|
789
828
|
|
|
790
829
|
// Chat-only: the user's base system prompt plus a freshly-generated
|
package/lib/core/modes.js
CHANGED
|
@@ -1259,7 +1259,19 @@
|
|
|
1259
1259
|
updateSelectionStatus();
|
|
1260
1260
|
return true;
|
|
1261
1261
|
}
|
|
1262
|
-
|
|
1262
|
+
// finalizeSimpleGeneration/finalizeModifyResult set prose:true on
|
|
1263
|
+
// EVERY agent-strategy final response, whether or not real
|
|
1264
|
+
// WRITE-tool work happened this run — it's the server's generic
|
|
1265
|
+
// "no classic flow/changes to review" signal, not a claim that
|
|
1266
|
+
// nothing happened. When real write results are attached
|
|
1267
|
+
// (data._agentWriteResults, added client-side in handleStep before
|
|
1268
|
+
// onDone fires), the caller's own deterministic-summary rendering
|
|
1269
|
+
// needs to run instead of this generic prose bubble — bug found
|
|
1270
|
+
// live during the 0.6.0 go-live smoke test: this unconditional
|
|
1271
|
+
// early-return made C1's entire deterministic-summary/prose-
|
|
1272
|
+
// demotion feature unreachable for every real agent-strategy run.
|
|
1273
|
+
var hasRealWriteResults = Array.isArray(data._agentWriteResults) && data._agentWriteResults.length > 0;
|
|
1274
|
+
if (data.prose && !hasRealWriteResults) {
|
|
1263
1275
|
if (looksLikeToolEnvelope(data.explanation)) {
|
|
1264
1276
|
handleExecuteError("FlowPilot's reply didn't come through as expected.", data.explanation);
|
|
1265
1277
|
updateSelectionStatus();
|
|
@@ -1320,7 +1332,7 @@
|
|
|
1320
1332
|
// executable flows (not documentation-only comment nodes) and when no
|
|
1321
1333
|
// loop is already active. The secondary "Just add to canvas" button is
|
|
1322
1334
|
// always shown alongside it as an escape hatch.
|
|
1323
|
-
var _hasDeployable = flow.some(function (n) { return n && n.type !== "comment" && n.type !== "group"; });
|
|
1335
|
+
var _hasDeployable = (flow || []).some(function (n) { return n && n.type !== "comment" && n.type !== "group"; });
|
|
1324
1336
|
var _buildOnImported = (goalPrompt && !activeBuildLoop && _hasDeployable)
|
|
1325
1337
|
? function (importResult) { startBuildLoop(goalPrompt, flow, importResult); }
|
|
1326
1338
|
: null;
|
|
@@ -2019,7 +2031,7 @@
|
|
|
2019
2031
|
// also runs verifyImportedNodes so the todo record still gets checked
|
|
2020
2032
|
// off. Without a build loop, fall back to a plain "Add to canvas"
|
|
2021
2033
|
// button that still fires the verify callback.
|
|
2022
|
-
var _hasDeployable = flow.some(function (n) { return n && n.type !== "comment" && n.type !== "group"; });
|
|
2034
|
+
var _hasDeployable = (flow || []).some(function (n) { return n && n.type !== "comment" && n.type !== "group"; });
|
|
2023
2035
|
var _wantLoop = goalPrompt && !activeBuildLoop && _hasDeployable;
|
|
2024
2036
|
var _onImported = _wantLoop
|
|
2025
2037
|
? function (importResult) {
|