channel-worker 2.5.39 → 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 +77 -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
|
|
@@ -1456,12 +1483,16 @@ class CommandPoller {
|
|
|
1456
1483
|
let parallelLimit = 2; // sane default if /workers/me fails or is
|
|
1457
1484
|
// unavailable on an older API; effective limit
|
|
1458
1485
|
// is still bounded by server-side enforcement.
|
|
1486
|
+
let independentAccounts = false; // when true: renderers are SEPARATE Flow
|
|
1487
|
+
// accounts → run all concurrently, skip the
|
|
1488
|
+
// shared-account round-robin close-idle rotation.
|
|
1459
1489
|
try {
|
|
1460
1490
|
if (this.api.getMyWorker) {
|
|
1461
1491
|
const me = await this.api.getMyWorker();
|
|
1462
1492
|
if (me && typeof me.parallel_limit === 'number') {
|
|
1463
1493
|
parallelLimit = Math.max(0, me.parallel_limit);
|
|
1464
1494
|
}
|
|
1495
|
+
if (me && me.independent_accounts) independentAccounts = true;
|
|
1465
1496
|
}
|
|
1466
1497
|
} catch (e) {
|
|
1467
1498
|
if (this.config.verbose) console.warn('[scene-dispatch] getMyWorker failed, using default:', e.message);
|
|
@@ -1472,7 +1503,19 @@ class CommandPoller {
|
|
|
1472
1503
|
&& (!r.pause_until || new Date(r.pause_until).getTime() > Date.now());
|
|
1473
1504
|
let stillOffline = renderers.filter(r => !runningRenderers.includes(r) && !isPaused(r));
|
|
1474
1505
|
const pausedCount = renderers.filter(isPaused).length;
|
|
1475
|
-
|
|
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
|
+
}
|
|
1476
1519
|
|
|
1477
1520
|
// ROUND-ROBIN ROTATION — when a running renderer just finished its
|
|
1478
1521
|
// scene (no in-flight cmd) AND there's at least one OFFLINE sibling
|
|
@@ -1483,7 +1526,13 @@ class CommandPoller {
|
|
|
1483
1526
|
// browser idle on Flow page keeps refreshing tokens / pinging
|
|
1484
1527
|
// telemetry → Veo flags "2 concurrent sessions per account" → captcha.
|
|
1485
1528
|
// Closing it = 0 idle sessions, only 1 active at a time.
|
|
1486
|
-
|
|
1529
|
+
// Round-robin close-idle is a SHARED-ACCOUNT safety (keep ≤1 Flow session
|
|
1530
|
+
// live per Google account). With independent_accounts every renderer is
|
|
1531
|
+
// its own account → skip it entirely and let them all run concurrently.
|
|
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) {
|
|
1487
1536
|
const stoppedNames = [];
|
|
1488
1537
|
for (const r of [...runningRenderers]) {
|
|
1489
1538
|
// Skip externally-launched profiles (user opened manually via NST UI)
|
|
@@ -1516,7 +1565,9 @@ class CommandPoller {
|
|
|
1516
1565
|
// the launcher picks the round-robin partner, not the one that just
|
|
1517
1566
|
// finished. API populates last_command_assigned_at on every claim,
|
|
1518
1567
|
// so this naturally implements turn-taking across siblings.
|
|
1519
|
-
|
|
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) => {
|
|
1520
1571
|
const ta = a.last_command_assigned_at ? new Date(a.last_command_assigned_at).getTime() : 0;
|
|
1521
1572
|
const tb = b.last_command_assigned_at ? new Date(b.last_command_assigned_at).getTime() : 0;
|
|
1522
1573
|
return ta - tb;
|
|
@@ -1536,11 +1587,11 @@ class CommandPoller {
|
|
|
1536
1587
|
const needNew = Math.max(0, queueCount);
|
|
1537
1588
|
const neededLaunches = Math.min(
|
|
1538
1589
|
parallelLimit - runningRenderers.length,
|
|
1539
|
-
|
|
1590
|
+
launchable.length,
|
|
1540
1591
|
needNew,
|
|
1541
1592
|
);
|
|
1542
1593
|
for (let li = 0; li < Math.max(0, neededLaunches); li++) {
|
|
1543
|
-
const toLaunch =
|
|
1594
|
+
const toLaunch = launchable[li];
|
|
1544
1595
|
console.log(`[scene-dispatch] Launching ${toLaunch.name} (${runningRenderers.length + li + 1}/${parallelLimit})`);
|
|
1545
1596
|
try {
|
|
1546
1597
|
await this._launchRendererProfile(toLaunch);
|
|
@@ -1786,14 +1837,33 @@ class CommandPoller {
|
|
|
1786
1837
|
const running = await this.nst.getRunningBrowsers();
|
|
1787
1838
|
if (running.length === 0) return;
|
|
1788
1839
|
|
|
1789
|
-
// 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).
|
|
1790
1845
|
const qi = await this.api.getSceneQueueCount();
|
|
1791
1846
|
const queueCount = typeof qi === 'number' ? qi : (qi?.total || 0);
|
|
1792
|
-
|
|
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;
|
|
1793
1860
|
|
|
1794
1861
|
for (const browser of running) {
|
|
1795
1862
|
const profileId = browser.profileId;
|
|
1796
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;
|
|
1797
1867
|
// Match by UUID or name (renderers use name as nst_profile_id)
|
|
1798
1868
|
const lastActivity = this._profileLastActivity[profileId]
|
|
1799
1869
|
|| (name && this._profileLastActivity[name])
|