vg-coder-cli 2.0.68 → 2.0.69
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/package.json
CHANGED
package/src/server/task-queue.js
CHANGED
|
@@ -125,6 +125,7 @@ class TaskQueue {
|
|
|
125
125
|
id: w.id,
|
|
126
126
|
email: w.email,
|
|
127
127
|
chromeId: w.chromeId || null,
|
|
128
|
+
tabId: typeof w.tabId === 'number' ? w.tabId : null,
|
|
128
129
|
status: w.status,
|
|
129
130
|
currentTaskId: w.currentTaskId,
|
|
130
131
|
cooldownUntil: cd,
|
|
@@ -180,6 +181,10 @@ class TaskQueue {
|
|
|
180
181
|
// Phase 1: chromeId comes from worker meta if extension passes it,
|
|
181
182
|
// else reverse-lookup from chromeIdToEmail. Phase 2 will require it.
|
|
182
183
|
existing.chromeId = meta?.chromeId || this._emailToChromeId(email) || existing.chromeId || null;
|
|
184
|
+
// tabId enables surgical close on recycle (only this tab vs all
|
|
185
|
+
// AI Studio tabs in the profile). Older extension versions may not
|
|
186
|
+
// send it — fall back to the previously stored value or null.
|
|
187
|
+
if (typeof meta?.tabId === 'number') existing.tabId = meta.tabId;
|
|
183
188
|
if (wasUnknown && email !== existing.email) {
|
|
184
189
|
// shouldn't happen — kept for clarity (email already assigned above)
|
|
185
190
|
}
|
|
@@ -202,6 +207,9 @@ class TaskQueue {
|
|
|
202
207
|
// Reverse-lookup from chromeIdToEmail fills the gap for already-bound
|
|
203
208
|
// profiles. Recycle/dispatch paths prefer chromeId when present.
|
|
204
209
|
chromeId: meta?.chromeId || this._emailToChromeId(email) || null,
|
|
210
|
+
// tabId from extension. When known, recycle closes only this tab —
|
|
211
|
+
// when null, recycle falls back to close-all-AI-Studio-tabs in profile.
|
|
212
|
+
tabId: typeof meta?.tabId === 'number' ? meta.tabId : null,
|
|
205
213
|
meta: { ...(meta || {}), registeredAt: Date.now() },
|
|
206
214
|
status: 'idle',
|
|
207
215
|
currentTaskId: null,
|
|
@@ -542,13 +550,14 @@ class TaskQueue {
|
|
|
542
550
|
}
|
|
543
551
|
const workerEmail = worker.email;
|
|
544
552
|
const workerChromeId = worker.chromeId || null;
|
|
553
|
+
const workerTabId = typeof worker.tabId === 'number' ? worker.tabId : null;
|
|
545
554
|
worker.currentTaskId = null;
|
|
546
555
|
worker.status = 'idle';
|
|
547
556
|
worker.lastSeen = Date.now();
|
|
548
557
|
|
|
549
558
|
if (task.cancelRequestedAt) {
|
|
550
559
|
// discard result; still recycle the tab so next task gets a clean slate
|
|
551
|
-
this._recycleWorkerTab(workerEmail, workerChromeId);
|
|
560
|
+
this._recycleWorkerTab(workerEmail, workerChromeId, workerTabId);
|
|
552
561
|
setImmediate(() => this._drain());
|
|
553
562
|
return;
|
|
554
563
|
}
|
|
@@ -569,7 +578,7 @@ class TaskQueue {
|
|
|
569
578
|
// model URL pinned. AI Studio's "+ New chat" link drops the ?model=
|
|
570
579
|
// query and Angular routerLink ignores href hijack — close+reopen via
|
|
571
580
|
// launcher is the only deterministic way to lock the model per task.
|
|
572
|
-
this._recycleWorkerTab(workerEmail, workerChromeId);
|
|
581
|
+
this._recycleWorkerTab(workerEmail, workerChromeId, workerTabId);
|
|
573
582
|
// Drain right away so other idle workers pick up queued tasks while
|
|
574
583
|
// this worker's tab is being recycled (~5s).
|
|
575
584
|
setImmediate(() => this._drain());
|
|
@@ -584,30 +593,32 @@ class TaskQueue {
|
|
|
584
593
|
}
|
|
585
594
|
const workerEmail = worker.email;
|
|
586
595
|
const workerChromeId = worker.chromeId || null;
|
|
596
|
+
const workerTabId = typeof worker.tabId === 'number' ? worker.tabId : null;
|
|
587
597
|
await this._failOrRequeue(task, worker, error);
|
|
588
598
|
// Only recycle if the worker isn't in cooldown (rate-limited workers
|
|
589
599
|
// have their tab still open intentionally — recycling won't unfreeze
|
|
590
600
|
// the quota and just wastes a tab cycle).
|
|
591
601
|
const w = this.workers.get(socketId);
|
|
592
602
|
if (!w || w.status !== 'rate_limited') {
|
|
593
|
-
this._recycleWorkerTab(workerEmail, workerChromeId);
|
|
603
|
+
this._recycleWorkerTab(workerEmail, workerChromeId, workerTabId);
|
|
594
604
|
} else {
|
|
595
605
|
setTimeout(() => this._drain(), INTER_TASK_GAP_MS);
|
|
596
606
|
}
|
|
597
607
|
}
|
|
598
608
|
|
|
599
609
|
/**
|
|
600
|
-
*
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
* later. _drain runs naturally on register so any queued task picks up
|
|
604
|
-
* the fresh worker. Fire-and-forget — errors are logged but don't bubble.
|
|
610
|
+
* Recycle the worker's tab: close it, then (unless close-only TTL) reopen
|
|
611
|
+
* a fresh one with the pinned model. Fire-and-forget — errors are logged
|
|
612
|
+
* but don't bubble.
|
|
605
613
|
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
608
|
-
*
|
|
614
|
+
* Routing precedence:
|
|
615
|
+
* - chromeId pins the exact Chrome profile (falls back to email if absent)
|
|
616
|
+
* - tabId pins the exact AI Studio tab in that profile, so user-opened
|
|
617
|
+
* tabs in the same profile are NOT closed alongside the worker tab.
|
|
618
|
+
* When tabId is null (older extension version), close-all behavior is
|
|
619
|
+
* used as before — backward compatible.
|
|
609
620
|
*/
|
|
610
|
-
_recycleWorkerTab(workerEmail, chromeId = null) {
|
|
621
|
+
_recycleWorkerTab(workerEmail, chromeId = null, tabId = null) {
|
|
611
622
|
if (!workerEmail || workerEmail.startsWith('unknown:')) return;
|
|
612
623
|
// Cancel any pending idle-close — we're about to recycle, no need to close
|
|
613
624
|
// separately. (This handles the race where a task lands during the idle
|
|
@@ -615,22 +626,28 @@ class TaskQueue {
|
|
|
615
626
|
this._cancelIdleClose(workerEmail);
|
|
616
627
|
// Mark workers in this profile as 'recycling' so _drain skips them. Match
|
|
617
628
|
// by chromeId when present (exact profile), else by email (covers the
|
|
618
|
-
// chromeId-unknown path).
|
|
629
|
+
// chromeId-unknown path). When tabId is known, only mark the worker for
|
|
630
|
+
// that specific tab — other workers in the same profile (other tabs)
|
|
631
|
+
// stay idle and can keep accepting tasks.
|
|
619
632
|
for (const w of this.workers.values()) {
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
633
|
+
let matches;
|
|
634
|
+
if (tabId != null) matches = w.tabId === tabId;
|
|
635
|
+
else if (chromeId) matches = w.chromeId === chromeId;
|
|
636
|
+
else matches = w.email === workerEmail;
|
|
623
637
|
if (matches && w.status === 'idle') w.status = 'recycling';
|
|
624
638
|
}
|
|
625
639
|
// Route launcher calls by chromeId when known, email otherwise.
|
|
626
640
|
const routeOpts = chromeId ? { chromeId } : { workerLabel: workerEmail };
|
|
627
641
|
const pinKey = chromeId || workerEmail;
|
|
642
|
+
// Surgical close: pass tabId so the launcher closes only the worker's
|
|
643
|
+
// tab. Without tabId, launcher falls back to close-all (legacy behavior).
|
|
644
|
+
const closePayload = tabId != null ? { tabId } : {};
|
|
628
645
|
// If WORKER_IDLE_TTL_MS === 1, treat post-task recycle as close-only:
|
|
629
646
|
// skip reopen; let _autoLaunchBrowser open a fresh tab when next task lands.
|
|
630
647
|
const closeOnly = WORKER_IDLE_TTL_MS === 1;
|
|
631
648
|
setImmediate(async () => {
|
|
632
649
|
try {
|
|
633
|
-
await this.requestLauncher('launcher:close_tab',
|
|
650
|
+
await this.requestLauncher('launcher:close_tab', closePayload, routeOpts, 5_000);
|
|
634
651
|
if (closeOnly) {
|
|
635
652
|
console.log(chalk.cyan(`[TaskQueue] Closed tab for ${workerEmail} (VG_WORKER_IDLE_TTL_MS=1)`));
|
|
636
653
|
return;
|
|
@@ -694,9 +711,35 @@ class TaskQueue {
|
|
|
694
711
|
for (const w of this.workers.values()) {
|
|
695
712
|
if (w.email === workerEmail && w.status !== 'idle') return;
|
|
696
713
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
714
|
+
// Surgical close: collect tabIds of all idle workers in this email
|
|
715
|
+
// and close them one-by-one so user-opened tabs in the same profile
|
|
716
|
+
// survive. If a worker has no tabId (older extension), fall back to
|
|
717
|
+
// close-all-by-email for that worker.
|
|
718
|
+
const tabIdsByLauncher = new Map(); // routeOpts JSON → [tabId, ...]
|
|
719
|
+
let needCloseAll = false;
|
|
720
|
+
for (const w of this.workers.values()) {
|
|
721
|
+
if (w.email !== workerEmail) continue;
|
|
722
|
+
if (w.status !== 'idle') continue;
|
|
723
|
+
const route = w.chromeId ? { chromeId: w.chromeId } : { workerLabel: workerEmail };
|
|
724
|
+
const key = JSON.stringify(route);
|
|
725
|
+
if (typeof w.tabId === 'number') {
|
|
726
|
+
if (!tabIdsByLauncher.has(key)) tabIdsByLauncher.set(key, { route, tabIds: [] });
|
|
727
|
+
tabIdsByLauncher.get(key).tabIds.push(w.tabId);
|
|
728
|
+
} else {
|
|
729
|
+
needCloseAll = true;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
console.log(chalk.gray(`[TaskQueue] Idle TTL expired for ${workerEmail} — closing tab(s)`));
|
|
733
|
+
if (needCloseAll) {
|
|
734
|
+
this.requestLauncher('launcher:close_tab', {}, { workerLabel: workerEmail }, 5_000)
|
|
735
|
+
.catch(err => console.log(chalk.yellow(`[TaskQueue] Idle close ${workerEmail} failed: ${err.message}`)));
|
|
736
|
+
}
|
|
737
|
+
for (const { route, tabIds } of tabIdsByLauncher.values()) {
|
|
738
|
+
for (const tabId of tabIds) {
|
|
739
|
+
this.requestLauncher('launcher:close_tab', { tabId }, route, 5_000)
|
|
740
|
+
.catch(err => console.log(chalk.yellow(`[TaskQueue] Idle close tab ${tabId} failed: ${err.message}`)));
|
|
741
|
+
}
|
|
742
|
+
}
|
|
700
743
|
}, WORKER_IDLE_TTL_MS);
|
|
701
744
|
if (t.unref) t.unref();
|
|
702
745
|
this._idleCloseTimers.set(workerEmail, t);
|
|
@@ -369,6 +369,10 @@ function connect() {
|
|
|
369
369
|
// Register with whatever email we have right now (may be null on cold load).
|
|
370
370
|
const initialEmail = extractEmail();
|
|
371
371
|
const chromeId = (window.vetgo && window.vetgo.chromeId) || null;
|
|
372
|
+
// tabId injected by background.ts during CONTROLLER script load.
|
|
373
|
+
// Server uses it for surgical tab close in _recycleWorkerTab so user
|
|
374
|
+
// tabs in the same profile don't get closed alongside the worker tab.
|
|
375
|
+
const tabId = (window.vetgo && typeof window.vetgo.tabId === 'number') ? window.vetgo.tabId : null;
|
|
372
376
|
// pinnedModel: lấy từ URL ?model=X — server dùng để re-establish pin
|
|
373
377
|
// sau khi worker register với email thật (không lúc open-tab vì lúc đó
|
|
374
378
|
// launcher có thể chưa scrape email).
|
|
@@ -382,9 +386,10 @@ function connect() {
|
|
|
382
386
|
userAgent: navigator.userAgent,
|
|
383
387
|
email: initialEmail,
|
|
384
388
|
chromeId,
|
|
389
|
+
tabId,
|
|
385
390
|
pinnedModel
|
|
386
391
|
});
|
|
387
|
-
console.log(`[TaskWorker] Initial email: ${initialEmail || '(pending)'}, chromeId: ${chromeId || '(none)'}`);
|
|
392
|
+
console.log(`[TaskWorker] Initial email: ${initialEmail || '(pending)'}, chromeId: ${chromeId || '(none)'}, tabId: ${tabId || '(none)'}`);
|
|
388
393
|
|
|
389
394
|
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
390
395
|
heartbeatTimer = setInterval(() => {
|
|
@@ -408,6 +413,7 @@ function connect() {
|
|
|
408
413
|
userAgent: navigator.userAgent,
|
|
409
414
|
email: resolved,
|
|
410
415
|
chromeId: (window.vetgo && window.vetgo.chromeId) || null,
|
|
416
|
+
tabId: (window.vetgo && typeof window.vetgo.tabId === 'number') ? window.vetgo.tabId : null,
|
|
411
417
|
pinnedModel: pm
|
|
412
418
|
});
|
|
413
419
|
console.log(`[TaskWorker] Re-registered with email: ${resolved} (pinnedModel=${pm || 'none'})`);
|