@poncho-ai/browser 0.6.24 → 0.6.26
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/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +33 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +57 -30
- package/package.json +1 -1
- package/src/session.ts +68 -30
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @poncho-ai/browser@0.6.
|
|
2
|
+
> @poncho-ai/browser@0.6.26 build /home/runner/work/poncho-ai/poncho-ai/packages/browser
|
|
3
3
|
> tsup src/index.ts --format esm --dts
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.ts
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
[34mCLI[39m tsup v8.5.1
|
|
8
8
|
[34mCLI[39m Target: es2022
|
|
9
9
|
[34mESM[39m Build start
|
|
10
|
-
[32mESM[39m [1mdist/index.js [22m[32m47.
|
|
11
|
-
[32mESM[39m ⚡️ Build success in
|
|
10
|
+
[32mESM[39m [1mdist/index.js [22m[32m47.98 KB[39m
|
|
11
|
+
[32mESM[39m ⚡️ Build success in 60ms
|
|
12
12
|
[34mDTS[39m Build start
|
|
13
|
-
[32mDTS[39m ⚡️ Build success in
|
|
14
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[32m13.
|
|
13
|
+
[32mDTS[39m ⚡️ Build success in 4894ms
|
|
14
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m13.77 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# @poncho-ai/browser
|
|
2
2
|
|
|
3
|
+
## 0.6.26
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#182](https://github.com/cesr/poncho-ai/pull/182) [`5ca3615`](https://github.com/cesr/poncho-ai/commit/5ca361576cbe1a97e6315f550a58a302b4e70aca) Thanks [@cesr](https://github.com/cesr)! - Keep host viewport listeners alive across browser sessions. `onFrame` /
|
|
8
|
+
`onStatus` listeners were stored inside the per-conversation `ConversationTab`
|
|
9
|
+
object, so `closeTab` (and LRU eviction) deleted them along with the tab. When
|
|
10
|
+
an agent closed one browser and opened another in the same conversation, the
|
|
11
|
+
new tab had empty listener sets — the host's live-viewport subscription was
|
|
12
|
+
silently orphaned, so the second session's `browser:status` / frames never
|
|
13
|
+
reached the client until it reconnected (the "pill/sheet doesn't appear, or is
|
|
14
|
+
left over after close, until I navigate away and back" bug). Listeners now live
|
|
15
|
+
in session-level maps keyed by conversationId, independent of any tab's
|
|
16
|
+
lifetime; they persist until the host unsubscribes, and `emitStatus` delivers
|
|
17
|
+
the final `active:false` on close before the tab is removed.
|
|
18
|
+
|
|
19
|
+
## 0.6.25
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- [#180](https://github.com/cesr/poncho-ai/pull/180) [`4e27887`](https://github.com/cesr/poncho-ai/commit/4e27887655eda1d420b8f69097cfc79e42f9596c) Thanks [@cesr](https://github.com/cesr)! - Fix a deadlock that wedged the browser session after the first lock-acquire
|
|
24
|
+
timeout. `lock()` pushed a wrapper closure onto `_lockQueue` but, on the 30s
|
|
25
|
+
timeout, tried to remove the entry with `indexOf(resolve)` — searching for a
|
|
26
|
+
different function — so the timed-out waiter was never spliced out. When the
|
|
27
|
+
current owner later called `unlock()`, it `shift()`ed that zombie waiter and
|
|
28
|
+
invoked it; `resolve()` on the already-rejected promise was a no-op, so the
|
|
29
|
+
unlock was consumed by a dead waiter, `_locked` stayed `true`, and no live
|
|
30
|
+
operation could ever acquire the lock again. Every subsequent browser call
|
|
31
|
+
then returned "Browser operation timed out waiting for lock (30s)" until the
|
|
32
|
+
session was torn down. Waiters are now tracked as objects with a `settled`
|
|
33
|
+
flag: a timed-out waiter removes itself from the queue, and `unlock()` skips
|
|
34
|
+
any already-settled waiters when handing off ownership.
|
|
35
|
+
|
|
3
36
|
## 0.6.24
|
|
4
37
|
|
|
5
38
|
### Patch Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -80,6 +80,8 @@ declare class BrowserSession {
|
|
|
80
80
|
private readonly sessionId;
|
|
81
81
|
private manager;
|
|
82
82
|
private readonly tabs;
|
|
83
|
+
private readonly frameListeners;
|
|
84
|
+
private readonly statusListeners;
|
|
83
85
|
private _contextStealthInstalled;
|
|
84
86
|
private readonly _uaOverrideApplied;
|
|
85
87
|
private _lockQueue;
|
package/dist/index.js
CHANGED
|
@@ -250,6 +250,15 @@ var BrowserSession = class {
|
|
|
250
250
|
manager;
|
|
251
251
|
// Tab management: conversationId → tab state
|
|
252
252
|
tabs = /* @__PURE__ */ new Map();
|
|
253
|
+
// Viewport listeners, keyed by conversationId and kept SEPARATE from the
|
|
254
|
+
// tab. A host (e.g. a live iOS viewport) subscribes once; its listeners must
|
|
255
|
+
// outlive any individual tab so that closing one browser and opening another
|
|
256
|
+
// in the same conversation — or an LRU tab eviction — doesn't silently
|
|
257
|
+
// orphan the subscription (the symptom: the second session's status/frames
|
|
258
|
+
// never reach the client until it reconnects). Tabs come and go; listeners
|
|
259
|
+
// persist until the host unsubscribes.
|
|
260
|
+
frameListeners = /* @__PURE__ */ new Map();
|
|
261
|
+
statusListeners = /* @__PURE__ */ new Map();
|
|
253
262
|
// Whether context-level stealth init script has been installed
|
|
254
263
|
_contextStealthInstalled = false;
|
|
255
264
|
// Track which tabs have had per-page CDP UA override applied
|
|
@@ -275,21 +284,33 @@ var BrowserSession = class {
|
|
|
275
284
|
return;
|
|
276
285
|
}
|
|
277
286
|
return new Promise((resolve2, reject) => {
|
|
287
|
+
const waiter = { settled: false, grant: () => {
|
|
288
|
+
} };
|
|
278
289
|
const timer = setTimeout(() => {
|
|
279
|
-
|
|
290
|
+
if (waiter.settled) return;
|
|
291
|
+
waiter.settled = true;
|
|
292
|
+
const idx = this._lockQueue.indexOf(waiter);
|
|
280
293
|
if (idx !== -1) this._lockQueue.splice(idx, 1);
|
|
281
294
|
reject(new Error("Browser operation timed out waiting for lock (30s)"));
|
|
282
295
|
}, 3e4);
|
|
283
|
-
|
|
296
|
+
waiter.grant = () => {
|
|
297
|
+
if (waiter.settled) return;
|
|
298
|
+
waiter.settled = true;
|
|
284
299
|
clearTimeout(timer);
|
|
285
300
|
resolve2();
|
|
286
|
-
}
|
|
301
|
+
};
|
|
302
|
+
this._lockQueue.push(waiter);
|
|
287
303
|
});
|
|
288
304
|
}
|
|
289
305
|
unlock() {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
306
|
+
while (this._lockQueue.length > 0) {
|
|
307
|
+
const next = this._lockQueue.shift();
|
|
308
|
+
if (!next.settled) {
|
|
309
|
+
next.grant();
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
this._locked = false;
|
|
293
314
|
}
|
|
294
315
|
// -----------------------------------------------------------------------
|
|
295
316
|
// Core browser + tab management
|
|
@@ -537,13 +558,10 @@ var BrowserSession = class {
|
|
|
537
558
|
if (realTabs > 0) {
|
|
538
559
|
await mgr.newTab();
|
|
539
560
|
}
|
|
540
|
-
const existing = tab;
|
|
541
561
|
tab = {
|
|
542
562
|
tabIndex: mgr.getActiveIndex(),
|
|
543
563
|
active: true,
|
|
544
|
-
lastUsed: Date.now()
|
|
545
|
-
frameListeners: existing?.frameListeners ?? /* @__PURE__ */ new Set(),
|
|
546
|
-
statusListeners: existing?.statusListeners ?? /* @__PURE__ */ new Set()
|
|
564
|
+
lastUsed: Date.now()
|
|
547
565
|
};
|
|
548
566
|
this.tabs.set(conversationId, tab);
|
|
549
567
|
} else {
|
|
@@ -817,15 +835,15 @@ var BrowserSession = class {
|
|
|
817
835
|
(frame) => {
|
|
818
836
|
const cid = this._screencastConversation;
|
|
819
837
|
if (!cid) return;
|
|
820
|
-
const
|
|
821
|
-
if (!
|
|
838
|
+
const listeners = this.frameListeners.get(cid);
|
|
839
|
+
if (!listeners || listeners.size === 0) return;
|
|
822
840
|
const browserFrame = {
|
|
823
841
|
data: frame.data,
|
|
824
842
|
width: frame.metadata.deviceWidth,
|
|
825
843
|
height: frame.metadata.deviceHeight,
|
|
826
844
|
timestamp: Date.now()
|
|
827
845
|
};
|
|
828
|
-
for (const listener of
|
|
846
|
+
for (const listener of listeners) {
|
|
829
847
|
try {
|
|
830
848
|
listener(browserFrame);
|
|
831
849
|
} catch {
|
|
@@ -853,29 +871,37 @@ var BrowserSession = class {
|
|
|
853
871
|
// Per-conversation event listeners
|
|
854
872
|
// -----------------------------------------------------------------------
|
|
855
873
|
onFrame(conversationId, listener) {
|
|
856
|
-
let
|
|
857
|
-
if (!
|
|
858
|
-
|
|
859
|
-
this.
|
|
874
|
+
let set = this.frameListeners.get(conversationId);
|
|
875
|
+
if (!set) {
|
|
876
|
+
set = /* @__PURE__ */ new Set();
|
|
877
|
+
this.frameListeners.set(conversationId, set);
|
|
860
878
|
}
|
|
861
|
-
|
|
879
|
+
set.add(listener);
|
|
862
880
|
return () => {
|
|
863
|
-
|
|
864
|
-
if (
|
|
865
|
-
|
|
866
|
-
|
|
881
|
+
const s = this.frameListeners.get(conversationId);
|
|
882
|
+
if (!s) return;
|
|
883
|
+
s.delete(listener);
|
|
884
|
+
if (s.size === 0) {
|
|
885
|
+
this.frameListeners.delete(conversationId);
|
|
886
|
+
if (this._screencastConversation === conversationId) {
|
|
887
|
+
this.stopScreencast().catch(() => {
|
|
888
|
+
});
|
|
889
|
+
}
|
|
867
890
|
}
|
|
868
891
|
};
|
|
869
892
|
}
|
|
870
893
|
onStatus(conversationId, listener) {
|
|
871
|
-
let
|
|
872
|
-
if (!
|
|
873
|
-
|
|
874
|
-
this.
|
|
894
|
+
let set = this.statusListeners.get(conversationId);
|
|
895
|
+
if (!set) {
|
|
896
|
+
set = /* @__PURE__ */ new Set();
|
|
897
|
+
this.statusListeners.set(conversationId, set);
|
|
875
898
|
}
|
|
876
|
-
|
|
899
|
+
set.add(listener);
|
|
877
900
|
return () => {
|
|
878
|
-
|
|
901
|
+
const s = this.statusListeners.get(conversationId);
|
|
902
|
+
if (!s) return;
|
|
903
|
+
s.delete(listener);
|
|
904
|
+
if (s.size === 0) this.statusListeners.delete(conversationId);
|
|
879
905
|
};
|
|
880
906
|
}
|
|
881
907
|
// -----------------------------------------------------------------------
|
|
@@ -1075,8 +1101,9 @@ var BrowserSession = class {
|
|
|
1075
1101
|
url: tab?.url,
|
|
1076
1102
|
interactionAllowed: tab?.active ?? false
|
|
1077
1103
|
};
|
|
1078
|
-
|
|
1079
|
-
|
|
1104
|
+
const listeners = this.statusListeners.get(conversationId);
|
|
1105
|
+
if (listeners) {
|
|
1106
|
+
for (const listener of listeners) {
|
|
1080
1107
|
try {
|
|
1081
1108
|
listener(status);
|
|
1082
1109
|
} catch {
|
package/package.json
CHANGED
package/src/session.ts
CHANGED
|
@@ -144,8 +144,6 @@ interface ConversationTab {
|
|
|
144
144
|
url?: string;
|
|
145
145
|
active: boolean;
|
|
146
146
|
lastUsed: number;
|
|
147
|
-
frameListeners: Set<FrameListener>;
|
|
148
|
-
statusListeners: Set<StatusListener>;
|
|
149
147
|
}
|
|
150
148
|
|
|
151
149
|
export class BrowserSession {
|
|
@@ -156,6 +154,16 @@ export class BrowserSession {
|
|
|
156
154
|
// Tab management: conversationId → tab state
|
|
157
155
|
private readonly tabs = new Map<string, ConversationTab>();
|
|
158
156
|
|
|
157
|
+
// Viewport listeners, keyed by conversationId and kept SEPARATE from the
|
|
158
|
+
// tab. A host (e.g. a live iOS viewport) subscribes once; its listeners must
|
|
159
|
+
// outlive any individual tab so that closing one browser and opening another
|
|
160
|
+
// in the same conversation — or an LRU tab eviction — doesn't silently
|
|
161
|
+
// orphan the subscription (the symptom: the second session's status/frames
|
|
162
|
+
// never reach the client until it reconnects). Tabs come and go; listeners
|
|
163
|
+
// persist until the host unsubscribes.
|
|
164
|
+
private readonly frameListeners = new Map<string, Set<FrameListener>>();
|
|
165
|
+
private readonly statusListeners = new Map<string, Set<StatusListener>>();
|
|
166
|
+
|
|
159
167
|
// Whether context-level stealth init script has been installed
|
|
160
168
|
private _contextStealthInstalled = false;
|
|
161
169
|
|
|
@@ -163,7 +171,7 @@ export class BrowserSession {
|
|
|
163
171
|
private readonly _uaOverrideApplied = new Set<string>();
|
|
164
172
|
|
|
165
173
|
// Serialization lock for tab-switching operations
|
|
166
|
-
private _lockQueue: Array<() => void> = [];
|
|
174
|
+
private _lockQueue: Array<{ grant: () => void; settled: boolean }> = [];
|
|
167
175
|
private _locked = false;
|
|
168
176
|
|
|
169
177
|
// Currently screencast conversation (only one at a time due to CDP)
|
|
@@ -189,19 +197,38 @@ export class BrowserSession {
|
|
|
189
197
|
return;
|
|
190
198
|
}
|
|
191
199
|
return new Promise<void>((resolve, reject) => {
|
|
200
|
+
const waiter = { settled: false, grant: () => {} };
|
|
192
201
|
const timer = setTimeout(() => {
|
|
193
|
-
|
|
202
|
+
if (waiter.settled) return;
|
|
203
|
+
waiter.settled = true;
|
|
204
|
+
const idx = this._lockQueue.indexOf(waiter);
|
|
194
205
|
if (idx !== -1) this._lockQueue.splice(idx, 1);
|
|
195
206
|
reject(new Error("Browser operation timed out waiting for lock (30s)"));
|
|
196
207
|
}, 30_000);
|
|
197
|
-
|
|
208
|
+
waiter.grant = () => {
|
|
209
|
+
if (waiter.settled) return;
|
|
210
|
+
waiter.settled = true;
|
|
211
|
+
clearTimeout(timer);
|
|
212
|
+
resolve();
|
|
213
|
+
};
|
|
214
|
+
this._lockQueue.push(waiter);
|
|
198
215
|
});
|
|
199
216
|
}
|
|
200
217
|
|
|
201
218
|
private unlock(): void {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
219
|
+
// Hand ownership to the next waiter that is still live. A waiter whose
|
|
220
|
+
// 30s timeout already fired has rejected its promise and marked itself
|
|
221
|
+
// settled; granting it would silently drop the lock (resolve is a no-op)
|
|
222
|
+
// and leave `_locked` true forever, wedging every later caller. Skip the
|
|
223
|
+
// dead ones; if none remain, release the lock.
|
|
224
|
+
while (this._lockQueue.length > 0) {
|
|
225
|
+
const next = this._lockQueue.shift()!;
|
|
226
|
+
if (!next.settled) {
|
|
227
|
+
next.grant();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
this._locked = false;
|
|
205
232
|
}
|
|
206
233
|
|
|
207
234
|
// -----------------------------------------------------------------------
|
|
@@ -481,13 +508,10 @@ export class BrowserSession {
|
|
|
481
508
|
if (realTabs > 0) {
|
|
482
509
|
await mgr.newTab();
|
|
483
510
|
}
|
|
484
|
-
const existing = tab;
|
|
485
511
|
tab = {
|
|
486
512
|
tabIndex: mgr.getActiveIndex(),
|
|
487
513
|
active: true,
|
|
488
514
|
lastUsed: Date.now(),
|
|
489
|
-
frameListeners: existing?.frameListeners ?? new Set(),
|
|
490
|
-
statusListeners: existing?.statusListeners ?? new Set(),
|
|
491
515
|
};
|
|
492
516
|
this.tabs.set(conversationId, tab);
|
|
493
517
|
} else {
|
|
@@ -780,15 +804,15 @@ export class BrowserSession {
|
|
|
780
804
|
(frame) => {
|
|
781
805
|
const cid = this._screencastConversation;
|
|
782
806
|
if (!cid) return;
|
|
783
|
-
const
|
|
784
|
-
if (!
|
|
807
|
+
const listeners = this.frameListeners.get(cid);
|
|
808
|
+
if (!listeners || listeners.size === 0) return;
|
|
785
809
|
const browserFrame: BrowserFrame = {
|
|
786
810
|
data: frame.data,
|
|
787
811
|
width: frame.metadata.deviceWidth,
|
|
788
812
|
height: frame.metadata.deviceHeight,
|
|
789
813
|
timestamp: Date.now(),
|
|
790
814
|
};
|
|
791
|
-
for (const listener of
|
|
815
|
+
for (const listener of listeners) {
|
|
792
816
|
try { listener(browserFrame); } catch { /* */ }
|
|
793
817
|
}
|
|
794
818
|
},
|
|
@@ -816,28 +840,38 @@ export class BrowserSession {
|
|
|
816
840
|
// -----------------------------------------------------------------------
|
|
817
841
|
|
|
818
842
|
onFrame(conversationId: string, listener: FrameListener): () => void {
|
|
819
|
-
let
|
|
820
|
-
if (!
|
|
821
|
-
|
|
822
|
-
this.
|
|
843
|
+
let set = this.frameListeners.get(conversationId);
|
|
844
|
+
if (!set) {
|
|
845
|
+
set = new Set();
|
|
846
|
+
this.frameListeners.set(conversationId, set);
|
|
823
847
|
}
|
|
824
|
-
|
|
848
|
+
set.add(listener);
|
|
825
849
|
return () => {
|
|
826
|
-
|
|
827
|
-
if (
|
|
828
|
-
|
|
850
|
+
const s = this.frameListeners.get(conversationId);
|
|
851
|
+
if (!s) return;
|
|
852
|
+
s.delete(listener);
|
|
853
|
+
if (s.size === 0) {
|
|
854
|
+
this.frameListeners.delete(conversationId);
|
|
855
|
+
if (this._screencastConversation === conversationId) {
|
|
856
|
+
this.stopScreencast().catch(() => {});
|
|
857
|
+
}
|
|
829
858
|
}
|
|
830
859
|
};
|
|
831
860
|
}
|
|
832
861
|
|
|
833
862
|
onStatus(conversationId: string, listener: StatusListener): () => void {
|
|
834
|
-
let
|
|
835
|
-
if (!
|
|
836
|
-
|
|
837
|
-
this.
|
|
863
|
+
let set = this.statusListeners.get(conversationId);
|
|
864
|
+
if (!set) {
|
|
865
|
+
set = new Set();
|
|
866
|
+
this.statusListeners.set(conversationId, set);
|
|
838
867
|
}
|
|
839
|
-
|
|
840
|
-
return () => {
|
|
868
|
+
set.add(listener);
|
|
869
|
+
return () => {
|
|
870
|
+
const s = this.statusListeners.get(conversationId);
|
|
871
|
+
if (!s) return;
|
|
872
|
+
s.delete(listener);
|
|
873
|
+
if (s.size === 0) this.statusListeners.delete(conversationId);
|
|
874
|
+
};
|
|
841
875
|
}
|
|
842
876
|
|
|
843
877
|
// -----------------------------------------------------------------------
|
|
@@ -1049,8 +1083,12 @@ export class BrowserSession {
|
|
|
1049
1083
|
url: tab?.url,
|
|
1050
1084
|
interactionAllowed: tab?.active ?? false,
|
|
1051
1085
|
};
|
|
1052
|
-
|
|
1053
|
-
|
|
1086
|
+
// Listeners live at the session level, so a close (which deletes the tab)
|
|
1087
|
+
// still delivers the final active:false to the host before the tab is
|
|
1088
|
+
// gone — and a later reopen reuses the same subscription.
|
|
1089
|
+
const listeners = this.statusListeners.get(conversationId);
|
|
1090
|
+
if (listeners) {
|
|
1091
|
+
for (const listener of listeners) {
|
|
1054
1092
|
try { listener(status); } catch { /* */ }
|
|
1055
1093
|
}
|
|
1056
1094
|
}
|