@browserbridge/bbx 1.7.8 → 1.9.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/PRIVACY.md +57 -12
- package/README.md +19 -8
- package/SECURITY.md +12 -0
- package/package.json +15 -6
- package/packages/agent-client/src/cli-args.js +59 -0
- package/packages/agent-client/src/cli-batch.js +121 -58
- package/packages/agent-client/src/cli-output.js +8 -1
- package/packages/agent-client/src/cli.js +272 -70
- package/packages/agent-client/src/client.js +296 -8
- package/packages/agent-client/src/command-registry.js +64 -55
- package/packages/agent-client/src/config.js +195 -0
- package/packages/agent-client/src/content-extract.js +198 -0
- package/packages/agent-client/src/npm-self-update.js +305 -0
- package/packages/agent-client/src/remotes.js +4 -2
- package/packages/agent-client/src/runtime.js +864 -39
- package/packages/agent-client/src/types.ts +169 -6
- package/packages/mcp-server/src/guidance.js +1 -1
- package/packages/mcp-server/src/handlers-capture.js +69 -6
- package/packages/mcp-server/src/handlers-dom.js +61 -4
- package/packages/mcp-server/src/handlers-navigation.js +2 -1
- package/packages/mcp-server/src/handlers-page.js +211 -54
- package/packages/mcp-server/src/handlers-utils.js +101 -10
- package/packages/mcp-server/src/handlers.js +14 -7
- package/packages/mcp-server/src/server.js +176 -11
- package/packages/native-host/src/artifact-store.js +353 -0
- package/packages/native-host/src/config.js +9 -1
- package/packages/native-host/src/daemon-logger.js +3 -1
- package/packages/native-host/src/daemon.js +701 -33
- package/packages/native-host/src/native-host.js +28 -3
- package/packages/protocol/src/capability-values.js +2 -0
- package/packages/protocol/src/defaults.js +31 -0
- package/packages/protocol/src/errors.js +102 -6
- package/packages/protocol/src/incidental-sanitizer.js +197 -0
- package/packages/protocol/src/index.js +2 -0
- package/packages/protocol/src/protocol.js +612 -6
- package/packages/protocol/src/recovery-telemetry.js +302 -0
- package/packages/protocol/src/registry.js +206 -38
- package/packages/protocol/src/summary.js +203 -30
- package/packages/protocol/src/types.ts +721 -2
- package/skills/browser-bridge/SKILL.md +54 -19
- package/skills/browser-bridge/references/interaction.md +55 -11
- package/skills/browser-bridge/references/patch-workflow.md +2 -0
- package/skills/browser-bridge/references/protocol.md +265 -86
- package/skills/browser-bridge/references/token-efficiency.md +29 -13
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
export const RECOVERY_WINDOW_MS = 5 * 60 * 1000;
|
|
4
|
+
export const RECOVERY_BUCKET_MS = 5 * 1000;
|
|
5
|
+
export const RECOVERY_LOOP_WINDOW_MS = 60 * 1000;
|
|
6
|
+
export const RECOVERY_LOOP_THRESHOLD = 3;
|
|
7
|
+
export const RECOVERY_MAX_GROUPS = 64;
|
|
8
|
+
export const RECOVERY_PUBLIC_COUNT_LIMIT = 10_000;
|
|
9
|
+
|
|
10
|
+
/** @type {readonly import('./types.js').RecoveryEventKind[]} */
|
|
11
|
+
export const RECOVERY_EVENT_KINDS = Object.freeze([
|
|
12
|
+
'automatic_mcp_retry',
|
|
13
|
+
'stale_ref_recovery',
|
|
14
|
+
'debugger_reattach',
|
|
15
|
+
'content_script_reinjection',
|
|
16
|
+
'native_host_reconnect',
|
|
17
|
+
'request_outcome',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const RECOVERY_EVENT_KIND_SET = new Set(RECOVERY_EVENT_KINDS);
|
|
21
|
+
const BUCKET_COUNT = RECOVERY_WINDOW_MS / RECOVERY_BUCKET_MS + 1;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Fixed-bucket, process-local recovery telemetry. Group names are used only to
|
|
25
|
+
* detect repeated failures and are never included in public snapshots.
|
|
26
|
+
*/
|
|
27
|
+
export class RecoveryTelemetryCollector {
|
|
28
|
+
/** @param {{ now?: () => number }} [options] */
|
|
29
|
+
constructor({ now = Date.now } = {}) {
|
|
30
|
+
this.now = now;
|
|
31
|
+
/** @type {Array<{ start: number, events: Map<import('./types.js').RecoveryEventKind, { attempts: number, successes: number, failures: number, lastEventAt: number }> } | null>} */
|
|
32
|
+
this.buckets = Array.from({ length: BUCKET_COUNT }, () => null);
|
|
33
|
+
/** @type {Map<string, { failures: [number, number, number], next: number, count: number, touchedAt: number, kind: import('./types.js').RecoveryEventKind }>} */
|
|
34
|
+
this.groups = new Map();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {import('./types.js').RecoveryEventKind} kind
|
|
39
|
+
* @param {'success' | 'failure'} outcome
|
|
40
|
+
* @param {string} [group='default']
|
|
41
|
+
*/
|
|
42
|
+
record(kind, outcome, group = 'default') {
|
|
43
|
+
if (!RECOVERY_EVENT_KIND_SET.has(kind)) return;
|
|
44
|
+
const now = normalizeNow(this.now());
|
|
45
|
+
const start = Math.floor(now / RECOVERY_BUCKET_MS) * RECOVERY_BUCKET_MS;
|
|
46
|
+
const index = Math.floor(start / RECOVERY_BUCKET_MS) % BUCKET_COUNT;
|
|
47
|
+
let bucket = this.buckets[index];
|
|
48
|
+
if (!bucket || bucket.start !== start) {
|
|
49
|
+
bucket = { start, events: new Map() };
|
|
50
|
+
this.buckets[index] = bucket;
|
|
51
|
+
}
|
|
52
|
+
const event = bucket.events.get(kind) ?? {
|
|
53
|
+
attempts: 0,
|
|
54
|
+
successes: 0,
|
|
55
|
+
failures: 0,
|
|
56
|
+
lastEventAt: 0,
|
|
57
|
+
};
|
|
58
|
+
event.attempts += 1;
|
|
59
|
+
event.successes += outcome === 'success' ? 1 : 0;
|
|
60
|
+
event.failures += outcome === 'failure' ? 1 : 0;
|
|
61
|
+
event.lastEventAt = now;
|
|
62
|
+
bucket.events.set(kind, event);
|
|
63
|
+
|
|
64
|
+
this.pruneGroups(now);
|
|
65
|
+
if (outcome !== 'failure') return;
|
|
66
|
+
const groupKey = `${kind}\u0000${group}`;
|
|
67
|
+
let state = this.groups.get(groupKey);
|
|
68
|
+
if (!state) {
|
|
69
|
+
while (this.groups.size >= RECOVERY_MAX_GROUPS) {
|
|
70
|
+
const oldest = [...this.groups].reduce((left, right) =>
|
|
71
|
+
left[1].touchedAt <= right[1].touchedAt ? left : right
|
|
72
|
+
);
|
|
73
|
+
this.groups.delete(oldest[0]);
|
|
74
|
+
}
|
|
75
|
+
state = {
|
|
76
|
+
failures: [0, 0, 0],
|
|
77
|
+
next: 0,
|
|
78
|
+
count: 0,
|
|
79
|
+
touchedAt: now,
|
|
80
|
+
kind,
|
|
81
|
+
};
|
|
82
|
+
this.groups.set(groupKey, state);
|
|
83
|
+
}
|
|
84
|
+
state.failures[state.next] = now;
|
|
85
|
+
state.next = (state.next + 1) % RECOVERY_LOOP_THRESHOLD;
|
|
86
|
+
state.count = Math.min(RECOVERY_LOOP_THRESHOLD, state.count + 1);
|
|
87
|
+
state.touchedAt = now;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** @param {import('./types.js').RecoveryScope} scope */
|
|
91
|
+
snapshot(scope) {
|
|
92
|
+
const now = normalizeNow(this.now());
|
|
93
|
+
this.pruneGroups(now);
|
|
94
|
+
/** @type {Record<import('./types.js').RecoveryEventKind, import('./types.js').RecoveryEventSummary>} */
|
|
95
|
+
const events =
|
|
96
|
+
/** @type {Record<import('./types.js').RecoveryEventKind, import('./types.js').RecoveryEventSummary>} */ ({});
|
|
97
|
+
for (const kind of RECOVERY_EVENT_KINDS) {
|
|
98
|
+
let attempts = 0;
|
|
99
|
+
let successes = 0;
|
|
100
|
+
let failures = 0;
|
|
101
|
+
let lastEventAt = 0;
|
|
102
|
+
for (const bucket of this.buckets) {
|
|
103
|
+
if (
|
|
104
|
+
!bucket ||
|
|
105
|
+
bucket.start + RECOVERY_BUCKET_MS <= now - RECOVERY_WINDOW_MS ||
|
|
106
|
+
bucket.start > now
|
|
107
|
+
) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const event = bucket.events.get(kind);
|
|
111
|
+
if (!event) continue;
|
|
112
|
+
attempts += event.attempts;
|
|
113
|
+
successes += event.successes;
|
|
114
|
+
failures += event.failures;
|
|
115
|
+
lastEventAt = Math.max(lastEventAt, event.lastEventAt);
|
|
116
|
+
}
|
|
117
|
+
const activeLoop = [...this.groups.values()].some((group) => {
|
|
118
|
+
if (group.kind !== kind || group.count < RECOVERY_LOOP_THRESHOLD) return false;
|
|
119
|
+
return group.failures.every((at) => at >= now - RECOVERY_LOOP_WINDOW_MS && at <= now);
|
|
120
|
+
});
|
|
121
|
+
events[kind] = createPublicEventSummary(
|
|
122
|
+
attempts,
|
|
123
|
+
successes,
|
|
124
|
+
failures,
|
|
125
|
+
activeLoop,
|
|
126
|
+
lastEventAt
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
scope,
|
|
131
|
+
windowMs: RECOVERY_WINDOW_MS,
|
|
132
|
+
bucketMs: RECOVERY_BUCKET_MS,
|
|
133
|
+
loopWindowMs: RECOVERY_LOOP_WINDOW_MS,
|
|
134
|
+
loopThreshold: RECOVERY_LOOP_THRESHOLD,
|
|
135
|
+
asOf: now,
|
|
136
|
+
activeLoop: RECOVERY_EVENT_KINDS.some((kind) => events[kind].activeLoop),
|
|
137
|
+
events,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** @param {number} now */
|
|
142
|
+
pruneGroups(now) {
|
|
143
|
+
const cutoff = now - RECOVERY_LOOP_WINDOW_MS;
|
|
144
|
+
for (const [key, group] of this.groups) {
|
|
145
|
+
let recent = 0;
|
|
146
|
+
for (let index = 0; index < group.count; index += 1) {
|
|
147
|
+
const at = group.failures[index];
|
|
148
|
+
if (at >= cutoff && at <= now) recent += 1;
|
|
149
|
+
}
|
|
150
|
+
if (recent === 0) this.groups.delete(key);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Strictly reconstruct a public summary from an untrusted transport value.
|
|
157
|
+
* @param {unknown} value
|
|
158
|
+
* @param {import('./types.js').RecoveryScope} expectedScope
|
|
159
|
+
* @returns {import('./types.js').RecoveryTelemetrySummary | null}
|
|
160
|
+
*/
|
|
161
|
+
export function normalizeRecoveryTelemetrySummary(value, expectedScope) {
|
|
162
|
+
const source = asRecord(value);
|
|
163
|
+
if (!source || source.scope !== expectedScope) return null;
|
|
164
|
+
const asOf = strictTimestamp(source.asOf);
|
|
165
|
+
if (asOf === null) return null;
|
|
166
|
+
const eventsSource = asRecord(source.events);
|
|
167
|
+
if (!eventsSource) return null;
|
|
168
|
+
/** @type {Record<import('./types.js').RecoveryEventKind, import('./types.js').RecoveryEventSummary>} */
|
|
169
|
+
const events =
|
|
170
|
+
/** @type {Record<import('./types.js').RecoveryEventKind, import('./types.js').RecoveryEventSummary>} */ ({});
|
|
171
|
+
for (const kind of RECOVERY_EVENT_KINDS) {
|
|
172
|
+
const event = asRecord(eventsSource[kind]);
|
|
173
|
+
if (!event) return null;
|
|
174
|
+
const attempts = rawCount(event.attempts);
|
|
175
|
+
const successes = rawCount(event.successes);
|
|
176
|
+
const failures = rawCount(event.failures);
|
|
177
|
+
const pending = rawCount(event.pending);
|
|
178
|
+
const saturated = event.saturated;
|
|
179
|
+
if (
|
|
180
|
+
attempts === null ||
|
|
181
|
+
successes === null ||
|
|
182
|
+
failures === null ||
|
|
183
|
+
pending === null ||
|
|
184
|
+
typeof saturated !== 'boolean' ||
|
|
185
|
+
successes + failures + pending !== attempts ||
|
|
186
|
+
(saturated
|
|
187
|
+
? attempts !== RECOVERY_PUBLIC_COUNT_LIMIT
|
|
188
|
+
: attempts > RECOVERY_PUBLIC_COUNT_LIMIT)
|
|
189
|
+
) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
const lastEventAt = normalizeLastEventAt(event.lastEventAt, attempts, asOf);
|
|
193
|
+
if (lastEventAt === undefined) return null;
|
|
194
|
+
events[kind] = {
|
|
195
|
+
attempts,
|
|
196
|
+
successes,
|
|
197
|
+
failures,
|
|
198
|
+
pending,
|
|
199
|
+
saturated,
|
|
200
|
+
successRate: saturated || attempts === 0 ? null : roundRate(successes / attempts),
|
|
201
|
+
failureRate: saturated || attempts === 0 ? null : roundRate(failures / attempts),
|
|
202
|
+
activeLoop: event.activeLoop === true && failures >= RECOVERY_LOOP_THRESHOLD,
|
|
203
|
+
lastEventAt,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
scope: expectedScope,
|
|
208
|
+
windowMs: RECOVERY_WINDOW_MS,
|
|
209
|
+
bucketMs: RECOVERY_BUCKET_MS,
|
|
210
|
+
loopWindowMs: RECOVERY_LOOP_WINDOW_MS,
|
|
211
|
+
loopThreshold: RECOVERY_LOOP_THRESHOLD,
|
|
212
|
+
asOf,
|
|
213
|
+
activeLoop: RECOVERY_EVENT_KINDS.some((kind) => events[kind].activeLoop),
|
|
214
|
+
events,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** @param {unknown} value */
|
|
219
|
+
function asRecord(value) {
|
|
220
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
221
|
+
? /** @type {Record<string, unknown>} */ (value)
|
|
222
|
+
: null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** @param {unknown} value @returns {number | null} */
|
|
226
|
+
function rawCount(value) {
|
|
227
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** @param {unknown} value @returns {number | null} */
|
|
231
|
+
function strictTimestamp(value) {
|
|
232
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** @param {number} value */
|
|
236
|
+
function normalizeNow(value) {
|
|
237
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* @param {unknown} value
|
|
242
|
+
* @param {number} attempts
|
|
243
|
+
* @param {number} asOf
|
|
244
|
+
* @returns {number | null | undefined}
|
|
245
|
+
*/
|
|
246
|
+
function normalizeLastEventAt(value, attempts, asOf) {
|
|
247
|
+
if (attempts === 0) return value === null ? null : undefined;
|
|
248
|
+
const timestamp = strictTimestamp(value);
|
|
249
|
+
if (
|
|
250
|
+
timestamp === null ||
|
|
251
|
+
timestamp > asOf ||
|
|
252
|
+
timestamp < asOf - RECOVERY_WINDOW_MS - RECOVERY_BUCKET_MS
|
|
253
|
+
) {
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
return timestamp;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** @param {number} value */
|
|
260
|
+
function roundRate(value) {
|
|
261
|
+
return Math.round(value * 10_000) / 10_000;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* @param {number} attempts
|
|
266
|
+
* @param {number} successes
|
|
267
|
+
* @param {number} failures
|
|
268
|
+
* @param {boolean} activeLoop
|
|
269
|
+
* @param {number} lastEventAt
|
|
270
|
+
* @returns {import('./types.js').RecoveryEventSummary}
|
|
271
|
+
*/
|
|
272
|
+
function createPublicEventSummary(attempts, successes, failures, activeLoop, lastEventAt) {
|
|
273
|
+
if (attempts <= RECOVERY_PUBLIC_COUNT_LIMIT) {
|
|
274
|
+
return {
|
|
275
|
+
attempts,
|
|
276
|
+
successes,
|
|
277
|
+
failures,
|
|
278
|
+
pending: 0,
|
|
279
|
+
saturated: false,
|
|
280
|
+
successRate: attempts > 0 ? roundRate(successes / attempts) : null,
|
|
281
|
+
failureRate: attempts > 0 ? roundRate(failures / attempts) : null,
|
|
282
|
+
activeLoop,
|
|
283
|
+
lastEventAt: attempts > 0 ? lastEventAt : null,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
const proportionalFailures = Math.floor((failures / attempts) * RECOVERY_PUBLIC_COUNT_LIMIT);
|
|
287
|
+
const cappedFailures = Math.min(
|
|
288
|
+
RECOVERY_PUBLIC_COUNT_LIMIT,
|
|
289
|
+
Math.max(activeLoop ? RECOVERY_LOOP_THRESHOLD : 0, proportionalFailures)
|
|
290
|
+
);
|
|
291
|
+
return {
|
|
292
|
+
attempts: RECOVERY_PUBLIC_COUNT_LIMIT,
|
|
293
|
+
successes: RECOVERY_PUBLIC_COUNT_LIMIT - cappedFailures,
|
|
294
|
+
failures: cappedFailures,
|
|
295
|
+
pending: 0,
|
|
296
|
+
saturated: true,
|
|
297
|
+
successRate: null,
|
|
298
|
+
failureRate: null,
|
|
299
|
+
activeLoop,
|
|
300
|
+
lastEventAt,
|
|
301
|
+
};
|
|
302
|
+
}
|