channel-worker 2.3.4 → 2.4.0

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/api-client.js CHANGED
@@ -83,6 +83,13 @@ class ApiClient {
83
83
  return this.request('GET', '/workers/scene-queue-count');
84
84
  }
85
85
 
86
+ // ChatGPT subscription dispatch — returns { count, profile_id } so the
87
+ // daemon knows whether to launch a dedicated capture profile (distinct
88
+ // from the Veo3/FlowKit renderers pool).
89
+ async getChatgptQueue() {
90
+ return this.request('GET', '/workers/chatgpt-queue-count');
91
+ }
92
+
86
93
  async sceneDispatch(nstProfileId) {
87
94
  return this.request('POST', '/workers/scene-dispatch', { nst_profile_id: nstProfileId });
88
95
  }
@@ -31,14 +31,21 @@ class CommandPoller {
31
31
  this._dispatchTimer = setInterval(() => this._dispatchScenes(), 5000);
32
32
  this._dispatching = false;
33
33
 
34
- // Profile timeoutclose idle profiles after 3 minutes with no work
35
- this._profileTimeoutTimer = setInterval(() => this._checkProfileTimeouts(), 60000);
34
+ // ChatGPT-subscription dispatcherlaunches the configured profile when
35
+ // a thumbnail command is waiting, independent from the scene renderers pool.
36
+ console.log('[chatgpt-dispatch] Started (every 5s)');
37
+ this._chatgptDispatchTimer = setInterval(() => this._dispatchChatgpt(), 5000);
38
+ this._chatgptDispatching = false;
39
+
40
+ // Profile timeout — close idle profiles after 30s with no work
41
+ this._profileTimeoutTimer = setInterval(() => this._checkProfileTimeouts(), 10000);
36
42
  this._profileLastActivity = {}; // { nst_profile_id: timestamp }
37
43
  }
38
44
 
39
45
  stop() {
40
46
  if (this.timer) { clearInterval(this.timer); this.timer = null; }
41
47
  if (this._dispatchTimer) { clearInterval(this._dispatchTimer); this._dispatchTimer = null; }
48
+ if (this._chatgptDispatchTimer) { clearInterval(this._chatgptDispatchTimer); this._chatgptDispatchTimer = null; }
42
49
  if (this._profileTimeoutTimer) { clearInterval(this._profileTimeoutTimer); this._profileTimeoutTimer = null; }
43
50
  }
44
51
 
@@ -1227,6 +1234,35 @@ class CommandPoller {
1227
1234
  const stillOffline = renderers.filter(r => !runningRenderers.includes(r));
1228
1235
  console.log(`[scene-dispatch] running=${runningRenderers.length}/${parallelLimit} (flowkit=${flowkitQ} dom=${domQ}) offline=${stillOffline.length} queue=${queueCount} names=[${runningRenderers.map(r=>r.name)}]`);
1229
1236
 
1237
+ // 5b. Close excess profiles — when parallelLimit drops (e.g., after a DOM→FlowKit
1238
+ // switch), idle profiles over the cap should shut down instead of staying open
1239
+ // and getting throttled at /flowkit/claim-command.
1240
+ if (runningRenderers.length > parallelLimit) {
1241
+ const excess = runningRenderers.length - parallelLimit;
1242
+ const idleRenderers = [];
1243
+ for (const r of runningRenderers) {
1244
+ // Only daemon-launched profiles are eligible — skip manual launches
1245
+ if (!this._profileLastActivity[r.nst_profile_id]) continue;
1246
+ try {
1247
+ const c = await this.api.rendererHasCommands(r.nst_profile_id);
1248
+ if (c === 0) idleRenderers.push(r);
1249
+ } catch {}
1250
+ }
1251
+ const toClose = idleRenderers.slice(0, excess);
1252
+ for (const r of toClose) {
1253
+ console.log(`[scene-dispatch] Closing excess profile ${r.name} (over cap ${parallelLimit})`);
1254
+ try {
1255
+ await this.nst.stopProfile(r.nst_profile_id);
1256
+ delete this._profileLastActivity[r.nst_profile_id];
1257
+ if (r.name) delete this._profileLastActivity[r.name.toLowerCase()];
1258
+ const idx = runningRenderers.indexOf(r);
1259
+ if (idx >= 0) runningRenderers.splice(idx, 1);
1260
+ } catch (e) {
1261
+ console.warn(`[scene-dispatch] Failed to close excess ${r.name}: ${e.message}`);
1262
+ }
1263
+ }
1264
+ }
1265
+
1230
1266
  // Count how many running profiles are actually free (no commands)
1231
1267
  let freeRunning = 0;
1232
1268
  for (const r of runningRenderers) {
@@ -1314,6 +1350,47 @@ class CommandPoller {
1314
1350
  await this.nst.launchProfile(renderer.nst_profile_id, { proxy: renderer.proxy || null, extensionPath, forceRelaunch: options.forceRelaunch });
1315
1351
  }
1316
1352
 
1353
+ // ─── ChatGPT Subscription Dispatch ─────────────────────────────────────────
1354
+ // Separate dispatcher for the dedicated ChatGPT profile (configured via
1355
+ // Setting: chatgpt_sub_profile_id). Launches the profile on-demand when a
1356
+ // generate_thumbnail command is pending, so Veo3 renderers stay untouched.
1357
+
1358
+ async _dispatchChatgpt() {
1359
+ if (this._chatgptDispatching) return;
1360
+ this._chatgptDispatching = true;
1361
+ try {
1362
+ if (!this.nst) return;
1363
+ const queue = await this.api.getChatgptQueue().catch(() => null);
1364
+ if (!queue || !queue.profile_id || queue.count <= 0) return;
1365
+
1366
+ const profileId = queue.profile_id;
1367
+ // Skip launch if already running (check via Nstbrowser running list).
1368
+ let alreadyRunning = false;
1369
+ try {
1370
+ const running = await this.nst.getRunningBrowsers();
1371
+ alreadyRunning = running.some(b => b.profileId === profileId || (b.name && b.name.toLowerCase() === profileId.toLowerCase()));
1372
+ } catch {}
1373
+ if (alreadyRunning) {
1374
+ this._profileLastActivity[profileId] = Date.now();
1375
+ return;
1376
+ }
1377
+
1378
+ console.log(`[chatgpt-dispatch] Launching ${profileId} (queue=${queue.count})`);
1379
+ try {
1380
+ // Reuse _launchRendererProfile contract — it just needs { nst_profile_id, os, proxy }.
1381
+ await this._launchRendererProfile({ nst_profile_id: profileId, os: 'windows', proxy: null });
1382
+ this._profileLastActivity[profileId] = Date.now();
1383
+ console.log(`[chatgpt-dispatch] ${profileId} launched`);
1384
+ } catch (err) {
1385
+ console.error(`[chatgpt-dispatch] Failed to launch ${profileId}: ${err.message}`);
1386
+ }
1387
+ } catch (err) {
1388
+ console.error(`[chatgpt-dispatch] Error: ${err.message}`);
1389
+ } finally {
1390
+ this._chatgptDispatching = false;
1391
+ }
1392
+ }
1393
+
1317
1394
  // ─── Profile Timeout ───────────────────────────────────────────────────────
1318
1395
  // Close profiles that have been idle (no commands assigned) for too long.
1319
1396
 
@@ -1321,7 +1398,7 @@ class CommandPoller {
1321
1398
  try {
1322
1399
  if (!this.nst) return;
1323
1400
 
1324
- const IDLE_TIMEOUT = 3 * 60 * 1000; // 3 minutes
1401
+ const IDLE_TIMEOUT = 30 * 1000; // 30s — matches old extension-side behavior
1325
1402
  const now = Date.now();
1326
1403
  const running = await this.nst.getRunningBrowsers();
1327
1404
  if (running.length === 0) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "channel-worker",
3
- "version": "2.3.4",
3
+ "version": "2.4.0",
4
4
  "description": "Channel Manager worker daemon — runs on remote machines to execute video pipeline jobs",
5
5
  "main": "lib/daemon.js",
6
6
  "bin": {