channel-worker 2.5.40 → 2.5.41
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/lib/command-poller.js +70 -7
- package/package.json +1 -1
package/lib/command-poller.js
CHANGED
|
@@ -1443,6 +1443,33 @@ class CommandPoller {
|
|
|
1443
1443
|
const domQ = typeof queueInfo === 'number' ? queueCount : (queueInfo?.dom_count || 0);
|
|
1444
1444
|
if (!queueCount) { this._dispatching = false; return; }
|
|
1445
1445
|
|
|
1446
|
+
// 4b. WHO can actually claim that queue. An idea pinned to one renderer
|
|
1447
|
+
// produces commands only that profile may claim — a raw total told us
|
|
1448
|
+
// "there's work, open another profile", the freshly-opened profile
|
|
1449
|
+
// claimed nothing, the round-robin below closed it as idle, and the
|
|
1450
|
+
// next cycle reopened it: a browser blinking open/closed every 5s
|
|
1451
|
+
// (observed on i5-pc: idea pinned to veo3-pro1, veo3-pro4 flapping).
|
|
1452
|
+
// unpinned_count === null means an older API that doesn't report the
|
|
1453
|
+
// breakdown → fall back to the previous "everything is claimable".
|
|
1454
|
+
const unpinnedCount = (queueInfo && typeof queueInfo.unpinned_count === 'number')
|
|
1455
|
+
? queueInfo.unpinned_count
|
|
1456
|
+
: null;
|
|
1457
|
+
const pinnedMap = (queueInfo && queueInfo.pinned && typeof queueInfo.pinned === 'object')
|
|
1458
|
+
? queueInfo.pinned
|
|
1459
|
+
: {};
|
|
1460
|
+
const pinnedFor = (r) => {
|
|
1461
|
+
const id = String(r?.nst_profile_id || '');
|
|
1462
|
+
if (!id) return 0;
|
|
1463
|
+
if (typeof pinnedMap[id] === 'number') return pinnedMap[id];
|
|
1464
|
+
const lower = id.toLowerCase();
|
|
1465
|
+
for (const k of Object.keys(pinnedMap)) {
|
|
1466
|
+
if (k.toLowerCase() === lower) return pinnedMap[k];
|
|
1467
|
+
}
|
|
1468
|
+
return 0;
|
|
1469
|
+
};
|
|
1470
|
+
// Can this renderer claim anything at all right now?
|
|
1471
|
+
const canClaim = (r) => (unpinnedCount === null) || unpinnedCount > 0 || pinnedFor(r) > 0;
|
|
1472
|
+
|
|
1446
1473
|
// 5. Parallel limit — sourced from THIS daemon's Worker.parallel_limit
|
|
1447
1474
|
// via /workers/me. Replaces the legacy global Settings
|
|
1448
1475
|
// (flowkit_max_concurrent / veo3_parallel_limit) which throttled
|
|
@@ -1476,7 +1503,19 @@ class CommandPoller {
|
|
|
1476
1503
|
&& (!r.pause_until || new Date(r.pause_until).getTime() > Date.now());
|
|
1477
1504
|
let stillOffline = renderers.filter(r => !runningRenderers.includes(r) && !isPaused(r));
|
|
1478
1505
|
const pausedCount = renderers.filter(isPaused).length;
|
|
1479
|
-
|
|
1506
|
+
const pinSummary = unpinnedCount === null
|
|
1507
|
+
? 'pins=n/a'
|
|
1508
|
+
: `unpinned=${unpinnedCount} pins=${JSON.stringify(pinnedMap)}`;
|
|
1509
|
+
console.log(`[scene-dispatch] running=${runningRenderers.length} cap=${parallelLimit} (flowkit=${flowkitQ} dom=${domQ}) offline=${stillOffline.length} paused=${pausedCount} queue=${queueCount} ${pinSummary} names=[${runningRenderers.map(r=>r.name)}]`);
|
|
1510
|
+
|
|
1511
|
+
// Offline renderers that could actually claim something. Everything else
|
|
1512
|
+
// must stay closed — opening it would only produce the flap described
|
|
1513
|
+
// above (open → claim nothing → closed as idle → reopen).
|
|
1514
|
+
let launchable = stillOffline.filter(canClaim);
|
|
1515
|
+
if (stillOffline.length > launchable.length) {
|
|
1516
|
+
const blocked = stillOffline.filter(r => !canClaim(r)).map(r => r.name || r.nst_profile_id);
|
|
1517
|
+
console.log(`[scene-dispatch] not launching [${blocked.join(',')}] — the whole queue is pinned to another renderer`);
|
|
1518
|
+
}
|
|
1480
1519
|
|
|
1481
1520
|
// ROUND-ROBIN ROTATION — when a running renderer just finished its
|
|
1482
1521
|
// scene (no in-flight cmd) AND there's at least one OFFLINE sibling
|
|
@@ -1490,7 +1529,10 @@ class CommandPoller {
|
|
|
1490
1529
|
// Round-robin close-idle is a SHARED-ACCOUNT safety (keep ≤1 Flow session
|
|
1491
1530
|
// live per Google account). With independent_accounts every renderer is
|
|
1492
1531
|
// its own account → skip it entirely and let them all run concurrently.
|
|
1493
|
-
|
|
1532
|
+
// Rotate only when there is a sibling that can actually take over the
|
|
1533
|
+
// remaining work — rotating toward a renderer the queue is not pinned to
|
|
1534
|
+
// just closes a useful profile and opens a useless one.
|
|
1535
|
+
if (!independentAccounts && queueCount > 0 && launchable.length > 0 && runningRenderers.length > 0) {
|
|
1494
1536
|
const stoppedNames = [];
|
|
1495
1537
|
for (const r of [...runningRenderers]) {
|
|
1496
1538
|
// Skip externally-launched profiles (user opened manually via NST UI)
|
|
@@ -1523,7 +1565,9 @@ class CommandPoller {
|
|
|
1523
1565
|
// the launcher picks the round-robin partner, not the one that just
|
|
1524
1566
|
// finished. API populates last_command_assigned_at on every claim,
|
|
1525
1567
|
// so this naturally implements turn-taking across siblings.
|
|
1526
|
-
|
|
1568
|
+
// Re-filter first: the round-robin above may have pushed a just-closed
|
|
1569
|
+
// renderer back into stillOffline.
|
|
1570
|
+
launchable = stillOffline.filter(canClaim).slice().sort((a, b) => {
|
|
1527
1571
|
const ta = a.last_command_assigned_at ? new Date(a.last_command_assigned_at).getTime() : 0;
|
|
1528
1572
|
const tb = b.last_command_assigned_at ? new Date(b.last_command_assigned_at).getTime() : 0;
|
|
1529
1573
|
return ta - tb;
|
|
@@ -1543,11 +1587,11 @@ class CommandPoller {
|
|
|
1543
1587
|
const needNew = Math.max(0, queueCount);
|
|
1544
1588
|
const neededLaunches = Math.min(
|
|
1545
1589
|
parallelLimit - runningRenderers.length,
|
|
1546
|
-
|
|
1590
|
+
launchable.length,
|
|
1547
1591
|
needNew,
|
|
1548
1592
|
);
|
|
1549
1593
|
for (let li = 0; li < Math.max(0, neededLaunches); li++) {
|
|
1550
|
-
const toLaunch =
|
|
1594
|
+
const toLaunch = launchable[li];
|
|
1551
1595
|
console.log(`[scene-dispatch] Launching ${toLaunch.name} (${runningRenderers.length + li + 1}/${parallelLimit})`);
|
|
1552
1596
|
try {
|
|
1553
1597
|
await this._launchRendererProfile(toLaunch);
|
|
@@ -1793,14 +1837,33 @@ class CommandPoller {
|
|
|
1793
1837
|
const running = await this.nst.getRunningBrowsers();
|
|
1794
1838
|
if (running.length === 0) return;
|
|
1795
1839
|
|
|
1796
|
-
// Check if there are any queued commands — if so, don't close anything
|
|
1840
|
+
// Check if there are any queued commands — if so, don't close anything.
|
|
1841
|
+
// Exception: a queue that is ENTIRELY pinned to specific renderers keeps
|
|
1842
|
+
// every other open profile idle forever, so it must not block the idle
|
|
1843
|
+
// close (each profile is still spared individually below if the queue
|
|
1844
|
+
// holds work pinned to it).
|
|
1797
1845
|
const qi = await this.api.getSceneQueueCount();
|
|
1798
1846
|
const queueCount = typeof qi === 'number' ? qi : (qi?.total || 0);
|
|
1799
|
-
|
|
1847
|
+
const unpinnedCount = (qi && typeof qi.unpinned_count === 'number') ? qi.unpinned_count : null;
|
|
1848
|
+
const pinnedMap = (qi && qi.pinned && typeof qi.pinned === 'object') ? qi.pinned : {};
|
|
1849
|
+
const pinnedForId = (id) => {
|
|
1850
|
+
if (!id) return 0;
|
|
1851
|
+
if (typeof pinnedMap[id] === 'number') return pinnedMap[id];
|
|
1852
|
+
const lower = String(id).toLowerCase();
|
|
1853
|
+
for (const k of Object.keys(pinnedMap)) {
|
|
1854
|
+
if (k.toLowerCase() === lower) return pinnedMap[k];
|
|
1855
|
+
}
|
|
1856
|
+
return 0;
|
|
1857
|
+
};
|
|
1858
|
+
// Old API (no breakdown) → previous behaviour: any queue blocks closing.
|
|
1859
|
+
if (queueCount > 0 && (unpinnedCount === null || unpinnedCount > 0)) return;
|
|
1800
1860
|
|
|
1801
1861
|
for (const browser of running) {
|
|
1802
1862
|
const profileId = browser.profileId;
|
|
1803
1863
|
const name = browser.name?.toLowerCase();
|
|
1864
|
+
// Queue holds work pinned to THIS profile → it's about to be claimed,
|
|
1865
|
+
// leave the browser open.
|
|
1866
|
+
if (pinnedForId(profileId) > 0 || pinnedForId(name) > 0) continue;
|
|
1804
1867
|
// Match by UUID or name (renderers use name as nst_profile_id)
|
|
1805
1868
|
const lastActivity = this._profileLastActivity[profileId]
|
|
1806
1869
|
|| (name && this._profileLastActivity[name])
|