@velanir/openclaw-browserbase 0.1.0 → 0.1.1

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/README.md CHANGED
@@ -27,9 +27,14 @@ browser tool ──cdpUrl http://127.0.0.1:<port>/?token=…──> broker (this
27
27
  - Every session is created with the coworker's persistent context
28
28
  (`persist: true`), the configured timeout/viewport/captcha/recording policy,
29
29
  and `userMetadata` tagging for observability and reaping.
30
- - Disconnect (task end, idle, gateway restart) releases the session. Login
31
- state survives in the context; the next browser action gets a fresh session
32
- that is already logged in.
30
+ - On downstream disconnect the broker **lingers**: the session and its
31
+ upstream connection are held (default 60s) so the next browser action
32
+ re-attaches instantly no new mint, no cooldown. OpenClaw's browser tool
33
+ opens short probe connections and reconnects between actions; without
34
+ linger every probe minted (and released) a billable session and the
35
+ post-release cooldown starved the real connection. Explicit release, idle
36
+ disconnect, linger expiry, and gateway shutdown all still release
37
+ deterministically; login state survives in the context either way.
33
38
  - One session per coworker context at a time (Browserbase forbids concurrent
34
39
  sessions on one context), with a short cooldown after release.
35
40
 
@@ -90,8 +95,8 @@ Notes:
90
95
  See `openclaw.plugin.json` for the full schema. Defaults implement the locked
91
96
  v1 policy: timeout 3600 s, viewport 1280×900, captcha solving on, recording on,
92
97
  `ignoreCertificateErrors: false`, per-coworker context with `persist: true`,
93
- cooldown 10 s, idle disconnect 15 min, reaper every 5 min, no proxies
94
- (`proxies: []` passes through to session creation when set).
98
+ linger 60 s, cooldown 3 s, idle disconnect 15 min, reaper every 5 min, no
99
+ proxies (`proxies: []` passes through to session creation when set).
95
100
 
96
101
  ## Agent tools
97
102
 
package/dist/index.js CHANGED
@@ -121,8 +121,18 @@ var BrowserbaseClient = class {
121
121
 
122
122
  // src/cdp-pipe.ts
123
123
  var INJECTED_ID_BASE = 19e8;
124
+ var INJECTED_ID_PIPE_STRIDE = 1e6;
125
+ var INJECTED_ID_PIPE_SLOTS = 200;
124
126
  var INJECTED_RESPONSE_MAX_BYTES = 4096;
125
127
  var DEFAULT_INJECTION_TIMEOUT_MS = 2e3;
128
+ var STALE_REPLY_TTL_MS = 15e3;
129
+ var MAX_TRACKED_CLIENT_IDS = 256;
130
+ var FRAME_HEAD_ID_RE = /"id"\s*:\s*(\d+)/;
131
+ function frameHeadId(data) {
132
+ const head = Buffer.isBuffer(data) ? data.subarray(0, 64).toString("utf8") : Array.isArray(data) ? Buffer.concat(data).subarray(0, 64).toString("utf8") : Buffer.from(data).subarray(0, 64).toString("utf8");
133
+ const match = FRAME_HEAD_ID_RE.exec(head);
134
+ return match ? Number(match[1]) : null;
135
+ }
126
136
  function rawDataByteLength(data) {
127
137
  if (Buffer.isBuffer(data)) {
128
138
  return data.byteLength;
@@ -141,38 +151,90 @@ function rawDataToString(data) {
141
151
  }
142
152
  return Buffer.from(data).toString("utf8");
143
153
  }
144
- var CdpPipe = class {
154
+ var CdpPipe = class _CdpPipe {
145
155
  constructor(downstream, upstream, opts) {
146
156
  this.downstream = downstream;
147
157
  this.upstream = upstream;
148
158
  this.opts = opts;
159
+ const ttl = Date.now() + STALE_REPLY_TTL_MS;
160
+ for (const id of opts.suppressStaleIds ?? []) {
161
+ this.suppressStale.set(id, ttl);
162
+ }
149
163
  downstream.on("message", (data, isBinary) => {
150
164
  this.lastActivityAt = Date.now();
165
+ if (!isBinary && this.clientPendingIds.size < MAX_TRACKED_CLIENT_IDS) {
166
+ const id = frameHeadId(data);
167
+ if (id !== null && id < INJECTED_ID_BASE) {
168
+ this.clientPendingIds.add(id);
169
+ }
170
+ }
151
171
  if (upstream.readyState === upstream.OPEN) {
152
172
  upstream.send(data, { binary: isBinary });
153
173
  }
154
174
  });
155
- upstream.on("message", (data, isBinary) => {
156
- this.lastActivityAt = Date.now();
157
- if (!isBinary && this.maybeResolveInjected(data)) {
158
- return;
159
- }
160
- if (downstream.readyState === downstream.OPEN) {
161
- downstream.send(data, { binary: isBinary });
162
- }
163
- });
175
+ upstream.on("message", this.onUpstreamMessage);
164
176
  downstream.on("close", () => this.close("downstream-closed"));
165
177
  downstream.on("error", () => this.close("downstream-error"));
166
- upstream.on("close", () => this.close("upstream-closed"));
167
- upstream.on("error", () => this.close("upstream-error"));
178
+ upstream.on("close", this.onUpstreamClose);
179
+ upstream.on("error", this.onUpstreamError);
168
180
  }
169
- nextInjectedId = INJECTED_ID_BASE;
181
+ static pipeSeq = 0;
182
+ nextInjectedId = INJECTED_ID_BASE + _CdpPipe.pipeSeq++ % INJECTED_ID_PIPE_SLOTS * INJECTED_ID_PIPE_STRIDE;
170
183
  pending = /* @__PURE__ */ new Map();
171
184
  closeReason = null;
185
+ upstreamClaimed = false;
186
+ /** Downstream command ids sent upstream with no reply seen yet. */
187
+ clientPendingIds = /* @__PURE__ */ new Set();
188
+ /** Stale ids from the previous client on this upstream → suppression deadline. */
189
+ suppressStale = /* @__PURE__ */ new Map();
172
190
  lastActivityAt = Date.now();
191
+ // Named so takeUpstream() can detach exactly the pipe's listeners from a
192
+ // claimed (lingering) upstream without disturbing the hold's own listeners.
193
+ onUpstreamMessage = (data, isBinary) => {
194
+ this.lastActivityAt = Date.now();
195
+ if (!isBinary) {
196
+ const id = frameHeadId(data);
197
+ if (id !== null) {
198
+ this.clientPendingIds.delete(id);
199
+ const deadline = this.suppressStale.get(id);
200
+ if (deadline !== void 0) {
201
+ this.suppressStale.delete(id);
202
+ if (Date.now() <= deadline) {
203
+ return;
204
+ }
205
+ }
206
+ }
207
+ if (this.maybeResolveInjected(data)) {
208
+ return;
209
+ }
210
+ }
211
+ if (this.downstream.readyState === this.downstream.OPEN) {
212
+ this.downstream.send(data, { binary: isBinary });
213
+ }
214
+ };
215
+ onUpstreamClose = () => this.close("upstream-closed");
216
+ onUpstreamError = () => this.close("upstream-error");
173
217
  get closed() {
174
218
  return this.closeReason !== null;
175
219
  }
220
+ /**
221
+ * Claim the upstream socket out of the pipe while it is still OPEN —
222
+ * callable from inside the onClose handler so the broker can keep the
223
+ * Browserbase session alive (linger) instead of releasing on downstream
224
+ * disconnect. Returns null when the upstream is already gone or claimed.
225
+ * staleClientIds are the old client's still-unanswered command ids; the
226
+ * next pipe on this upstream must suppress their late replies.
227
+ */
228
+ takeUpstream() {
229
+ if (this.upstreamClaimed || this.upstream.readyState !== this.upstream.OPEN) {
230
+ return null;
231
+ }
232
+ this.upstreamClaimed = true;
233
+ this.upstream.off("message", this.onUpstreamMessage);
234
+ this.upstream.off("close", this.onUpstreamClose);
235
+ this.upstream.off("error", this.onUpstreamError);
236
+ return { socket: this.upstream, staleClientIds: [...this.clientPendingIds] };
237
+ }
176
238
  /** Send a broker-owned CDP command on the piped connection and await its reply. */
177
239
  inject(method, params) {
178
240
  if (this.closeReason !== null || this.upstream.readyState !== this.upstream.OPEN) {
@@ -228,7 +290,9 @@ var CdpPipe = class {
228
290
  entry.reject(new Error(`CDP pipe closed: ${reason}`));
229
291
  }
230
292
  this.pending.clear();
231
- for (const socket of [this.downstream, this.upstream]) {
293
+ this.opts.onClose(reason);
294
+ const sockets = this.upstreamClaimed ? [this.downstream] : [this.downstream, this.upstream];
295
+ for (const socket of sockets) {
232
296
  if (socket.readyState === socket.OPEN || socket.readyState === socket.CLOSING) {
233
297
  try {
234
298
  socket.close(1e3);
@@ -239,7 +303,6 @@ var CdpPipe = class {
239
303
  socket.terminate();
240
304
  }
241
305
  }
242
- this.opts.onClose(reason);
243
306
  }
244
307
  };
245
308
 
@@ -250,8 +313,9 @@ var DEFAULT_TIMEOUT_SECONDS = 3600;
250
313
  var MIN_TIMEOUT_SECONDS = 60;
251
314
  var MAX_TIMEOUT_SECONDS = 21600;
252
315
  var DEFAULT_VIEWPORT = { width: 1280, height: 900 };
253
- var DEFAULT_CONTEXT_COOLDOWN_SECONDS = 10;
316
+ var DEFAULT_CONTEXT_COOLDOWN_SECONDS = 3;
254
317
  var DEFAULT_CONTEXT_WAIT_MS = 1e4;
318
+ var DEFAULT_LINGER_SECONDS = 60;
255
319
  var DEFAULT_IDLE_DISCONNECT_MINUTES = 15;
256
320
  var DEFAULT_REAPER_INTERVAL_MINUTES = 5;
257
321
  var DEFAULT_API_BASE_URL = "https://api.browserbase.com";
@@ -368,6 +432,7 @@ function normalizeConfig(raw) {
368
432
  waitMs: readNumber(context.waitMs, DEFAULT_CONTEXT_WAIT_MS, 0, 6e4)
369
433
  },
370
434
  proxies: readProxies(root.proxies),
435
+ lingerSeconds: readNumber(root.lingerSeconds, DEFAULT_LINGER_SECONDS, 0, 600),
371
436
  idleDisconnectMinutes: readNumber(root.idleDisconnectMinutes, DEFAULT_IDLE_DISCONNECT_MINUTES, 0, 1440),
372
437
  reaperIntervalMinutes: readNumber(root.reaperIntervalMinutes, DEFAULT_REAPER_INTERVAL_MINUTES, 0, 1440),
373
438
  apiBaseUrl: readString(root.apiBaseUrl) ?? DEFAULT_API_BASE_URL,
@@ -752,7 +817,7 @@ var BrowserbaseBroker = class {
752
817
  if (liveness.gone) {
753
818
  liveness.detach();
754
819
  socket.destroy();
755
- this.holdForAdoption(minted, held.upstream);
820
+ this.holdForAdoption(minted, held.upstream, { staleClientIds: held.staleClientIds ?? [] });
756
821
  return;
757
822
  }
758
823
  let upstream = held.upstream && held.upstream.readyState === held.upstream.OPEN ? held.upstream : null;
@@ -777,15 +842,16 @@ var BrowserbaseBroker = class {
777
842
  if (liveness.gone) {
778
843
  liveness.detach();
779
844
  socket.destroy();
780
- this.holdForAdoption(minted, upstream);
845
+ this.holdForAdoption(minted, upstream, { staleClientIds: held.staleClientIds ?? [] });
781
846
  return;
782
847
  }
783
848
  liveness.detach();
849
+ const staleClientIds = held.staleClientIds ?? [];
784
850
  this.wss.handleUpgrade(req, socket, head, (downstream) => {
785
- this.attachPipe(downstream, upstream, minted);
851
+ this.attachPipe(downstream, upstream, minted, staleClientIds);
786
852
  });
787
853
  }
788
- attachPipe(downstream, upstream, minted) {
854
+ attachPipe(downstream, upstream, minted, staleClientIds = []) {
789
855
  if (upstream.readyState !== upstream.OPEN || downstream.readyState !== downstream.OPEN) {
790
856
  upstream.terminate();
791
857
  if (downstream.readyState === downstream.OPEN) {
@@ -798,12 +864,14 @@ var BrowserbaseBroker = class {
798
864
  return;
799
865
  }
800
866
  const pipe = new CdpPipe(downstream, upstream, {
801
- onClose: (reason) => this.onPipeClosed(minted.sessionId, reason)
867
+ onClose: (reason) => this.onPipeClosed(minted.sessionId, reason),
868
+ ...staleClientIds.length ? { suppressStaleIds: staleClientIds } : {}
802
869
  });
803
870
  this.active = {
804
871
  sessionId: minted.sessionId,
805
872
  leaseId: minted.leaseId,
806
873
  contextId: minted.contextId,
874
+ connectUrl: minted.connectUrl,
807
875
  ...minted.expiresAt ? { expiresAt: minted.expiresAt } : {},
808
876
  startedAt: Date.now(),
809
877
  pipe
@@ -813,10 +881,29 @@ var BrowserbaseBroker = class {
813
881
  );
814
882
  }
815
883
  onPipeClosed(sessionId, reason) {
816
- if (this.active?.sessionId !== sessionId) {
884
+ const active = this.active;
885
+ if (active?.sessionId !== sessionId) {
817
886
  return;
818
887
  }
819
888
  this.active = null;
889
+ const downstreamGone = reason === "downstream-closed" || reason === "downstream-error";
890
+ if (downstreamGone && !this.stopping && this.config.lingerSeconds > 0) {
891
+ const claimed = active.pipe.takeUpstream();
892
+ if (claimed) {
893
+ this.holdForAdoption(
894
+ {
895
+ sessionId: active.sessionId,
896
+ connectUrl: active.connectUrl,
897
+ contextId: active.contextId,
898
+ leaseId: active.leaseId,
899
+ ...active.expiresAt ? { expiresAt: active.expiresAt } : {}
900
+ },
901
+ claimed.socket,
902
+ { windowMs: this.config.lingerSeconds * 1e3, reason: "linger", staleClientIds: claimed.staleClientIds }
903
+ );
904
+ return;
905
+ }
906
+ }
820
907
  this.cooldownUntil = Date.now() + this.config.context.cooldownSeconds * 1e3;
821
908
  this.logger.info?.(`session ${sessionId} disconnected (${reason}); releasing`);
822
909
  void this.releaseQuietly(sessionId, reason).finally(() => {
@@ -907,18 +994,21 @@ var BrowserbaseBroker = class {
907
994
  });
908
995
  });
909
996
  }
910
- holdForAdoption(session, upstream) {
997
+ holdForAdoption(session, upstream, opts = {}) {
998
+ const reason = opts.reason ?? "adoption";
999
+ const windowMs = opts.windowMs ?? ADOPTION_WINDOW_MS;
911
1000
  this.logger.info?.(
912
- `holding session ${session.sessionId} for adoption (client disconnected mid-handshake, upstream ${upstream ? "open" : "not dialed"})`
1001
+ `holding session ${session.sessionId} (${reason}, ${Math.round(windowMs / 1e3)}s window, upstream ${upstream ? "open" : "not dialed"})`
913
1002
  );
914
1003
  const timer = setTimeout(() => {
915
1004
  if (this.adoptable?.session.sessionId === session.sessionId) {
916
1005
  const held = this.adoptable;
917
1006
  this.adoptable = null;
918
1007
  held.upstream?.terminate();
919
- void this.releaseQuietly(session.sessionId, "adoption-expired");
1008
+ this.cooldownUntil = Date.now() + this.config.context.cooldownSeconds * 1e3;
1009
+ void this.releaseQuietly(session.sessionId, `${reason}-expired`);
920
1010
  }
921
- }, ADOPTION_WINDOW_MS);
1011
+ }, windowMs);
922
1012
  timer.unref();
923
1013
  let onUpstreamLost;
924
1014
  if (upstream) {
@@ -926,7 +1016,8 @@ var BrowserbaseBroker = class {
926
1016
  if (this.adoptable?.session.sessionId === session.sessionId && this.adoptable.upstream === upstream) {
927
1017
  clearTimeout(this.adoptable.timer);
928
1018
  this.adoptable = null;
929
- void this.releaseQuietly(session.sessionId, "adoption-upstream-lost");
1019
+ this.cooldownUntil = Date.now() + this.config.context.cooldownSeconds * 1e3;
1020
+ void this.releaseQuietly(session.sessionId, `${reason}-upstream-lost`);
930
1021
  }
931
1022
  };
932
1023
  upstream.once("close", onUpstreamLost);
@@ -934,7 +1025,9 @@ var BrowserbaseBroker = class {
934
1025
  }
935
1026
  this.adoptable = {
936
1027
  session,
1028
+ heldReason: reason,
937
1029
  ...upstream ? { upstream } : {},
1030
+ ...opts.staleClientIds?.length ? { staleClientIds: opts.staleClientIds } : {},
938
1031
  ...onUpstreamLost ? { onUpstreamLost } : {},
939
1032
  timer
940
1033
  };
@@ -952,7 +1045,8 @@ var BrowserbaseBroker = class {
952
1045
  this.adoptable = null;
953
1046
  return {
954
1047
  session: held.session,
955
- ...held.upstream ? { upstream: held.upstream } : {}
1048
+ ...held.upstream ? { upstream: held.upstream } : {},
1049
+ ...held.staleClientIds?.length ? { staleClientIds: held.staleClientIds } : {}
956
1050
  };
957
1051
  }
958
1052
  // ------------------------------------------------------------- cleanup --
@@ -1019,24 +1113,32 @@ var BrowserbaseBroker = class {
1019
1113
  startedAt: new Date(active.startedAt).toISOString(),
1020
1114
  ...active.expiresAt ? { expiresAt: active.expiresAt } : {},
1021
1115
  idleMs: Date.now() - active.pipe.lastActivityAt
1022
- } : null
1116
+ } : null,
1117
+ held: this.adoptable ? { sessionId: this.adoptable.session.sessionId, reason: this.adoptable.heldReason } : null
1023
1118
  };
1024
1119
  }
1025
1120
  async getLiveViewUrls() {
1026
- const active = this.active;
1027
- if (!active || active.pipe.closed) {
1121
+ const sessionId = this.active && !this.active.pipe.closed ? this.active.sessionId : this.adoptable?.upstream ? this.adoptable.session.sessionId : null;
1122
+ if (!sessionId) {
1028
1123
  return null;
1029
1124
  }
1030
- return this.client.getDebugUrls(active.sessionId);
1125
+ return this.client.getDebugUrls(sessionId);
1031
1126
  }
1032
1127
  /** Returns true when there was an active session to release. */
1033
1128
  releaseActiveSession() {
1034
1129
  const active = this.active;
1035
- if (!active || active.pipe.closed) {
1036
- return false;
1130
+ if (active && !active.pipe.closed) {
1131
+ active.pipe.close("manual-release");
1132
+ return true;
1037
1133
  }
1038
- active.pipe.close("manual-release");
1039
- return true;
1134
+ const held = this.takeAdoptable();
1135
+ if (held) {
1136
+ held.upstream?.terminate();
1137
+ this.cooldownUntil = Date.now() + this.config.context.cooldownSeconds * 1e3;
1138
+ void this.releaseQuietly(held.session.sessionId, "manual-release");
1139
+ return true;
1140
+ }
1141
+ return false;
1040
1142
  }
1041
1143
  };
1042
1144
 
@@ -1063,7 +1165,7 @@ function createBrowserbaseTools(getBroker) {
1063
1165
  const status = broker.getStatus();
1064
1166
  if (!status.active) {
1065
1167
  const lines2 = [
1066
- NO_SESSION,
1168
+ status.held?.reason === "linger" ? `Browserbase session ${status.held.sessionId} is lingering for instant resume \u2014 the next browser action re-attaches to it (same tabs may be gone, login state intact).` : NO_SESSION,
1067
1169
  `Region: ${status.region}. Recording: ${status.recordSession ? "on" : "off"}.`,
1068
1170
  status.contextId ? `Browser context: ${status.contextId}` : "Browser context: created on first use."
1069
1171
  ];
@@ -8,7 +8,6 @@
8
8
  "configSchema": {
9
9
  "type": "object",
10
10
  "additionalProperties": false,
11
- "required": ["projectId", "apiKey", "listenPort"],
12
11
  "properties": {
13
12
  "projectId": {
14
13
  "type": "string",
@@ -20,7 +19,12 @@
20
19
  },
21
20
  "region": {
22
21
  "type": "string",
23
- "enum": ["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"],
22
+ "enum": [
23
+ "us-west-2",
24
+ "us-east-1",
25
+ "eu-central-1",
26
+ "ap-southeast-1"
27
+ ],
24
28
  "default": "us-west-2"
25
29
  },
26
30
  "listenPort": {
@@ -38,7 +42,9 @@
38
42
  },
39
43
  "metadata": {
40
44
  "type": "object",
41
- "additionalProperties": { "type": "string" }
45
+ "additionalProperties": {
46
+ "type": "string"
47
+ }
42
48
  },
43
49
  "defaults": {
44
50
  "type": "object",
@@ -54,28 +60,63 @@
54
60
  "type": "object",
55
61
  "additionalProperties": false,
56
62
  "properties": {
57
- "width": { "type": "number", "minimum": 320, "maximum": 3840 },
58
- "height": { "type": "number", "minimum": 320, "maximum": 2160 }
63
+ "width": {
64
+ "type": "number",
65
+ "minimum": 320,
66
+ "maximum": 3840
67
+ },
68
+ "height": {
69
+ "type": "number",
70
+ "minimum": 320,
71
+ "maximum": 2160
72
+ }
59
73
  }
60
74
  },
61
- "solveCaptchas": { "type": "boolean", "default": true },
62
- "recordSession": { "type": "boolean", "default": true },
63
- "logSession": { "type": "boolean", "default": true },
64
- "ignoreCertificateErrors": { "type": "boolean", "default": false }
75
+ "solveCaptchas": {
76
+ "type": "boolean",
77
+ "default": true
78
+ },
79
+ "recordSession": {
80
+ "type": "boolean",
81
+ "default": true
82
+ },
83
+ "logSession": {
84
+ "type": "boolean",
85
+ "default": true
86
+ },
87
+ "ignoreCertificateErrors": {
88
+ "type": "boolean",
89
+ "default": false
90
+ }
65
91
  }
66
92
  },
67
93
  "context": {
68
94
  "type": "object",
69
95
  "additionalProperties": false,
70
96
  "properties": {
71
- "persist": { "type": "boolean", "default": true },
72
- "cooldownSeconds": { "type": "number", "default": 10, "minimum": 0, "maximum": 120 },
73
- "waitMs": { "type": "number", "default": 10000, "minimum": 0, "maximum": 60000 }
97
+ "persist": {
98
+ "type": "boolean",
99
+ "default": true
100
+ },
101
+ "cooldownSeconds": {
102
+ "type": "number",
103
+ "default": 3,
104
+ "minimum": 0,
105
+ "maximum": 120
106
+ },
107
+ "waitMs": {
108
+ "type": "number",
109
+ "default": 10000,
110
+ "minimum": 0,
111
+ "maximum": 60000
112
+ }
74
113
  }
75
114
  },
76
115
  "proxies": {
77
116
  "type": "array",
78
- "items": { "type": "object" },
117
+ "items": {
118
+ "type": "object"
119
+ },
79
120
  "default": []
80
121
  },
81
122
  "idleDisconnectMinutes": {
@@ -87,6 +128,12 @@
87
128
  "type": "number",
88
129
  "default": 5,
89
130
  "minimum": 0
131
+ },
132
+ "lingerSeconds": {
133
+ "type": "number",
134
+ "default": 60,
135
+ "minimum": 0,
136
+ "maximum": 600
90
137
  }
91
138
  }
92
139
  },
@@ -120,6 +167,10 @@
120
167
  "idleDisconnectMinutes": {
121
168
  "label": "Idle Disconnect (min)",
122
169
  "help": "Close and release the Browserbase session after this many minutes without CDP traffic. 0 disables. Login state survives via the persistent context."
170
+ },
171
+ "lingerSeconds": {
172
+ "label": "Linger (s)",
173
+ "help": "Keep the session and its connection alive after a disconnect so the next browser action re-attaches instantly instead of minting a new session. 0 releases immediately."
123
174
  }
124
175
  }
125
176
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velanir/openclaw-browserbase",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "OpenClaw plugin that brokers Browserbase sessions behind a loopback CDP endpoint: explicit session creation with per-coworker persistent contexts, deterministic release, and stale-session reaping.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",