channel-worker 2.4.3 → 2.4.5

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.
@@ -1176,18 +1176,30 @@ class CommandPoller {
1176
1176
  const offlineRenderers = renderers.filter(r => !runningRenderers.includes(r));
1177
1177
 
1178
1178
  // 3. Crash recovery
1179
- // 3a. Offline profiles with assigned commands → reset + relaunch
1179
+ // 3a. Offline profiles with assigned commands → reset + relaunch.
1180
+ // BUT: Nstbrowser API can briefly drop a profile from its running list
1181
+ // during heavy ops (image gen, captcha solving). The reset endpoint
1182
+ // returns { skipped: 'heartbeat-fresh' } if the extension is still
1183
+ // sending heartbeats — meaning the profile is alive even though
1184
+ // Nstbrowser doesn't list it. Skip relaunch in that case to avoid
1185
+ // a tabs-close + tabs-open loop.
1180
1186
  for (const r of offlineRenderers) {
1181
1187
  try {
1182
1188
  const cmdCount = await this.api.rendererHasCommands(r.nst_profile_id);
1183
1189
  if (cmdCount > 0) {
1184
- console.log(`[scene-dispatch] Crash recovery: ${r.name} has ${cmdCount} commands but not running — relaunching`);
1190
+ console.log(`[scene-dispatch] Crash recovery: ${r.name} has ${cmdCount} commands but not running — checking heartbeat`);
1185
1191
  try {
1186
- await this.api.resetRendererCommands(r.nst_profile_id);
1192
+ const resetRes = await this.api.resetRendererCommands(r.nst_profile_id);
1193
+ if (resetRes && resetRes.skipped === 'heartbeat-fresh') {
1194
+ console.log(`[scene-dispatch] ${r.name} heartbeat fresh — extension is alive, skipping relaunch`);
1195
+ runningRenderers.push(r);
1196
+ this._profileLastActivity[r.nst_profile_id] = Date.now();
1197
+ continue;
1198
+ }
1187
1199
  await this._launchRendererProfile(r);
1188
1200
  runningRenderers.push(r);
1189
1201
  this._profileLastActivity[r.nst_profile_id] = Date.now();
1190
- console.log(`[scene-dispatch] ${r.name} recovered (${cmdCount} commands reset)`);
1202
+ console.log(`[scene-dispatch] ${r.name} recovered (${(resetRes && resetRes.modified) || cmdCount} commands reset)`);
1191
1203
  } catch (err) {
1192
1204
  console.error(`[scene-dispatch] Failed to recover ${r.name}: ${err.message}`);
1193
1205
  }
@@ -1454,6 +1466,21 @@ class CommandPoller {
1454
1466
  // User-launched profiles have no tracking entry — leave them alone.
1455
1467
  if (!lastActivity) continue;
1456
1468
  if ((now - lastActivity) > IDLE_TIMEOUT) {
1469
+ // Per-profile in-flight check — getSceneQueueCount only counts
1470
+ // QUEUED cmds, not RUNNING ones. Without this, a profile actively
1471
+ // running a master cast / scene cmd gets closed mid-task because
1472
+ // the queue is empty after the cmd was claimed.
1473
+ let inFlight = 0;
1474
+ try {
1475
+ const probeId = name || profileId;
1476
+ inFlight = await this.api.rendererHasCommands(probeId);
1477
+ } catch {}
1478
+ if (inFlight > 0) {
1479
+ // Bump activity so we re-evaluate after another IDLE_TIMEOUT.
1480
+ this._profileLastActivity[profileId] = now;
1481
+ if (name) this._profileLastActivity[name] = now;
1482
+ continue;
1483
+ }
1457
1484
  console.log(`[profile-timeout] Closing idle profile ${browser.name || profileId} (idle ${Math.round((now - lastActivity) / 1000)}s)`);
1458
1485
  try {
1459
1486
  await this.nst.stopProfile(profileId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "channel-worker",
3
- "version": "2.4.3",
3
+ "version": "2.4.5",
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": {