@trusty-squire/mcp 1.1.13 → 1.1.14-rc.10
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 +94 -110
- package/{LICENSE → assets/licenses/browser-use-MIT.txt} +1 -1
- package/dist/api-client.d.ts +9 -0
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +11 -0
- package/dist/api-client.js.map +1 -1
- package/dist/bin.js +7 -1
- package/dist/bin.js.map +1 -1
- package/dist/bot/broker/admission-context.d.ts +11 -0
- package/dist/bot/broker/admission-context.d.ts.map +1 -0
- package/dist/bot/broker/admission-context.js +17 -0
- package/dist/bot/broker/admission-context.js.map +1 -0
- package/dist/bot/broker/authority.d.ts +76 -0
- package/dist/bot/broker/authority.d.ts.map +1 -0
- package/dist/bot/broker/authority.js +528 -0
- package/dist/bot/broker/authority.js.map +1 -0
- package/dist/bot/broker/custody.d.ts +20 -0
- package/dist/bot/broker/custody.d.ts.map +1 -0
- package/dist/bot/broker/custody.js +10 -0
- package/dist/bot/broker/custody.js.map +1 -0
- package/dist/bot/broker/daemon.d.ts +24 -0
- package/dist/bot/broker/daemon.d.ts.map +1 -0
- package/dist/bot/broker/daemon.js +371 -0
- package/dist/bot/broker/daemon.js.map +1 -0
- package/dist/bot/broker/discovery.d.ts +9 -0
- package/dist/bot/broker/discovery.d.ts.map +1 -0
- package/dist/bot/broker/discovery.js +167 -0
- package/dist/bot/broker/discovery.js.map +1 -0
- package/dist/bot/broker/dispatch-journal.d.ts +71 -0
- package/dist/bot/broker/dispatch-journal.d.ts.map +1 -0
- package/dist/bot/broker/dispatch-journal.js +319 -0
- package/dist/bot/broker/dispatch-journal.js.map +1 -0
- package/dist/bot/broker/forwarder.d.ts +35 -0
- package/dist/bot/broker/forwarder.d.ts.map +1 -0
- package/dist/bot/broker/forwarder.js +150 -0
- package/dist/bot/broker/forwarder.js.map +1 -0
- package/dist/bot/broker/identity-lane.d.ts +3 -0
- package/dist/bot/broker/identity-lane.d.ts.map +1 -0
- package/dist/bot/broker/identity-lane.js +15 -0
- package/dist/bot/broker/identity-lane.js.map +1 -0
- package/dist/bot/broker/lineage.d.ts +3 -0
- package/dist/bot/broker/lineage.d.ts.map +1 -0
- package/dist/bot/broker/lineage.js +16 -0
- package/dist/bot/broker/lineage.js.map +1 -0
- package/dist/bot/broker/maintenance.d.ts +4 -0
- package/dist/bot/broker/maintenance.d.ts.map +1 -0
- package/dist/bot/broker/maintenance.js +49 -0
- package/dist/bot/broker/maintenance.js.map +1 -0
- package/dist/bot/broker/operator.d.ts +48 -0
- package/dist/bot/broker/operator.d.ts.map +1 -0
- package/dist/bot/broker/operator.js +523 -0
- package/dist/bot/broker/operator.js.map +1 -0
- package/dist/bot/broker/qualification.d.ts +5 -0
- package/dist/bot/broker/qualification.d.ts.map +1 -0
- package/dist/bot/broker/qualification.js +67 -0
- package/dist/bot/broker/qualification.js.map +1 -0
- package/dist/bot/broker/runtime.d.ts +36 -0
- package/dist/bot/broker/runtime.d.ts.map +1 -0
- package/dist/bot/broker/runtime.js +202 -0
- package/dist/bot/broker/runtime.js.map +1 -0
- package/dist/bot/broker/scheduler.d.ts +20 -0
- package/dist/bot/broker/scheduler.d.ts.map +1 -0
- package/dist/bot/broker/scheduler.js +94 -0
- package/dist/bot/broker/scheduler.js.map +1 -0
- package/dist/bot/broker/transport.d.ts +27 -0
- package/dist/bot/broker/transport.d.ts.map +1 -0
- package/dist/bot/broker/transport.js +265 -0
- package/dist/bot/broker/transport.js.map +1 -0
- package/dist/bot/browser-process-owner.d.ts +75 -0
- package/dist/bot/browser-process-owner.d.ts.map +1 -0
- package/dist/bot/browser-process-owner.js +939 -0
- package/dist/bot/browser-process-owner.js.map +1 -0
- package/dist/bot/browser-process-runtime.d.ts +168 -0
- package/dist/bot/browser-process-runtime.d.ts.map +1 -0
- package/dist/bot/browser-process-runtime.js +940 -0
- package/dist/bot/browser-process-runtime.js.map +1 -0
- package/dist/bot/browser-use-capture.d.ts +15 -0
- package/dist/bot/browser-use-capture.d.ts.map +1 -0
- package/dist/bot/browser-use-capture.js +975 -0
- package/dist/bot/browser-use-capture.js.map +1 -0
- package/dist/bot/browser-use-paint-order.d.ts +12 -0
- package/dist/bot/browser-use-paint-order.d.ts.map +1 -0
- package/dist/bot/browser-use-paint-order.js +73 -0
- package/dist/bot/browser-use-paint-order.js.map +1 -0
- package/dist/bot/browser-use-serializer.d.ts +67 -0
- package/dist/bot/browser-use-serializer.d.ts.map +1 -0
- package/dist/bot/browser-use-serializer.js +1027 -0
- package/dist/bot/browser-use-serializer.js.map +1 -0
- package/dist/bot/browser.d.ts +158 -264
- package/dist/bot/browser.d.ts.map +1 -1
- package/dist/bot/browser.js +1681 -2830
- package/dist/bot/browser.js.map +1 -1
- package/dist/bot/compact-observation-v2.d.ts +54 -45
- package/dist/bot/compact-observation-v2.d.ts.map +1 -1
- package/dist/bot/compact-observation-v2.js +454 -165
- package/dist/bot/compact-observation-v2.js.map +1 -1
- package/dist/bot/credential-shape.d.ts.map +1 -1
- package/dist/bot/credential-shape.js +7 -0
- package/dist/bot/credential-shape.js.map +1 -1
- package/dist/bot/identity-runtime.d.ts +56 -0
- package/dist/bot/identity-runtime.d.ts.map +1 -0
- package/dist/bot/identity-runtime.js +141 -0
- package/dist/bot/identity-runtime.js.map +1 -0
- package/dist/bot/mutation-dispatch-evidence.d.ts +13 -0
- package/dist/bot/mutation-dispatch-evidence.d.ts.map +1 -0
- package/dist/bot/mutation-dispatch-evidence.js +19 -0
- package/dist/bot/mutation-dispatch-evidence.js.map +1 -0
- package/dist/bot/owned-pages.d.ts +16 -0
- package/dist/bot/owned-pages.d.ts.map +1 -0
- package/dist/bot/owned-pages.js +58 -0
- package/dist/bot/owned-pages.js.map +1 -0
- package/dist/bot/page-driver.d.ts +37 -0
- package/dist/bot/page-driver.d.ts.map +1 -0
- package/dist/bot/page-driver.js +303 -0
- package/dist/bot/page-driver.js.map +1 -0
- package/dist/bot/pay-operator.d.ts +1 -0
- package/dist/bot/pay-operator.d.ts.map +1 -1
- package/dist/bot/pay-operator.js +10 -0
- package/dist/bot/pay-operator.js.map +1 -1
- package/dist/bot/profile.d.ts +9 -0
- package/dist/bot/profile.d.ts.map +1 -1
- package/dist/bot/profile.js +16 -11
- package/dist/bot/profile.js.map +1 -1
- package/dist/bot/provision-session.d.ts +32 -7
- package/dist/bot/provision-session.d.ts.map +1 -1
- package/dist/bot/provision-session.js +1008 -471
- package/dist/bot/provision-session.js.map +1 -1
- package/dist/bot/session/lifecycle.d.ts +5 -2
- package/dist/bot/session/lifecycle.d.ts.map +1 -1
- package/dist/bot/session/lifecycle.js +303 -34
- package/dist/bot/session/lifecycle.js.map +1 -1
- package/dist/bot/session/multisession-flag.d.ts +2 -0
- package/dist/bot/session/multisession-flag.d.ts.map +1 -0
- package/dist/bot/session/multisession-flag.js +23 -0
- package/dist/bot/session/multisession-flag.js.map +1 -0
- package/dist/install/cli.d.ts.map +1 -1
- package/dist/install/cli.js +2 -1
- package/dist/install/cli.js.map +1 -1
- package/dist/server-instance-registry.d.ts +18 -6
- package/dist/server-instance-registry.d.ts.map +1 -1
- package/dist/server-instance-registry.js +49 -28
- package/dist/server-instance-registry.js.map +1 -1
- package/dist/server.d.ts +6 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +122 -29
- package/dist/server.js.map +1 -1
- package/dist/tools/audit-log.d.ts.map +1 -1
- package/dist/tools/audit-log.js +1 -5
- package/dist/tools/audit-log.js.map +1 -1
- package/dist/tools/audit-rollup.d.ts +2 -5
- package/dist/tools/audit-rollup.d.ts.map +1 -1
- package/dist/tools/audit-rollup.js +0 -0
- package/dist/tools/audit-rollup.js.map +1 -1
- package/dist/tools/credential-mutations.d.ts +16 -16
- package/dist/tools/index.js +2 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/operate-pay.d.ts +18 -18
- package/dist/tools/provision-drive.d.ts +263 -555
- package/dist/tools/provision-drive.d.ts.map +1 -1
- package/dist/tools/provision-drive.js +404 -1139
- package/dist/tools/provision-drive.js.map +1 -1
- package/dist/tools/use-credential.d.ts +6 -6
- package/package.json +1 -1
|
@@ -1,35 +1,169 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
|
+
import { createHmac, randomBytes } from "node:crypto";
|
|
2
3
|
export const OBSERVE_V2_MAX_WIRE_BYTES = 4_096;
|
|
3
4
|
export const OBSERVE_V2_MAX_TOKENS = 1_024;
|
|
4
|
-
const FIRST_PAGE_ROW_LIMIT = 4;
|
|
5
5
|
export function compactV2PayloadWithinBudget(payload) {
|
|
6
6
|
const bytes = Buffer.byteLength(JSON.stringify(payload), "utf8");
|
|
7
|
-
|
|
7
|
+
if (bytes > OBSERVE_V2_MAX_WIRE_BYTES)
|
|
8
|
+
return false;
|
|
9
|
+
// Token estimate at the ~4 bytes/token the wire's enum-only prose runs.
|
|
10
|
+
// Comparing raw BYTES against the token cap (the old behavior) made the
|
|
11
|
+
// effective budget 1024 bytes, so a real-world OAuth-shaped URL alone
|
|
12
|
+
// hard-failed observation — the live "compact-v2 budget metadata exceeded"
|
|
13
|
+
// session killer. The byte cap bounds the MCP message; the token cap bounds
|
|
14
|
+
// model context, and both now scale with the page instead of strangling it.
|
|
15
|
+
return Math.ceil(bytes / 4) <= OBSERVE_V2_MAX_TOKENS;
|
|
8
16
|
}
|
|
9
17
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
18
|
+
* Graceful degradation for a payload whose FIXED metadata (url, semantic
|
|
19
|
+
* context, start hints) alone exceeds the wire budget. Degrades in order:
|
|
20
|
+
* shrink the URL, drop the semantic title/heading, drop start-routing hints —
|
|
21
|
+
* never the safe_table rows or the overflow cursor, which are the actionable
|
|
22
|
+
* map. Returns null only when code-owned fields (session id / cursor) are
|
|
23
|
+
* themselves hostile, which the caller fails closed on.
|
|
13
24
|
*/
|
|
14
|
-
export function
|
|
15
|
-
|
|
25
|
+
export function compactV2DegradeMetadata(payload) {
|
|
26
|
+
if (compactV2PayloadWithinBudget(payload))
|
|
27
|
+
return payload;
|
|
28
|
+
let candidate = payload;
|
|
29
|
+
for (const urlCap of [512, 128]) {
|
|
30
|
+
const url = typeof candidate.url === "string" ? candidate.url : "";
|
|
31
|
+
if (url.length > urlCap)
|
|
32
|
+
candidate = { ...candidate, url: url.slice(0, urlCap) };
|
|
33
|
+
if (compactV2PayloadWithinBudget(candidate))
|
|
34
|
+
return candidate;
|
|
35
|
+
}
|
|
36
|
+
if ("semantic" in candidate) {
|
|
37
|
+
const rest = { ...candidate };
|
|
38
|
+
delete rest.semantic;
|
|
39
|
+
candidate = rest;
|
|
40
|
+
if (compactV2PayloadWithinBudget(candidate))
|
|
41
|
+
return candidate;
|
|
42
|
+
}
|
|
43
|
+
if ("hint" in candidate || "user_email" in candidate || "hint_overflow" in candidate) {
|
|
44
|
+
const rest = { ...candidate };
|
|
45
|
+
delete rest.hint;
|
|
46
|
+
delete rest.user_email;
|
|
47
|
+
delete rest.hint_overflow;
|
|
48
|
+
candidate = rest;
|
|
49
|
+
if (compactV2PayloadWithinBudget(candidate))
|
|
50
|
+
return candidate;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
16
53
|
}
|
|
17
|
-
|
|
18
|
-
export const COMPACT_V2_HANDLE_LENGTH = 10;
|
|
54
|
+
export const COMPACT_V2_HANDLE_LENGTH = 22;
|
|
19
55
|
const COMPACT_V2_HANDLE_RE = new RegExp(`^@e:[A-Za-z0-9_-]{${COMPACT_V2_HANDLE_LENGTH}}$`);
|
|
20
|
-
|
|
56
|
+
export class StableObservationRefs {
|
|
57
|
+
secret;
|
|
58
|
+
document;
|
|
59
|
+
generation = 0;
|
|
60
|
+
refs = new Map();
|
|
61
|
+
identities = new Map();
|
|
62
|
+
anchors = new Map();
|
|
63
|
+
aliases = new Map();
|
|
64
|
+
aliasOwners = new Map();
|
|
65
|
+
reset(document) {
|
|
66
|
+
if (this.document === document)
|
|
67
|
+
return;
|
|
68
|
+
this.document = document;
|
|
69
|
+
this.generation++;
|
|
70
|
+
this.refs.clear();
|
|
71
|
+
this.identities.clear();
|
|
72
|
+
this.anchors.clear();
|
|
73
|
+
this.aliases.clear();
|
|
74
|
+
this.aliasOwners.clear();
|
|
75
|
+
}
|
|
76
|
+
/** Reconcile the full live inventory, retiring absent or materially changed nodes. */
|
|
77
|
+
actions(document, elements) {
|
|
78
|
+
this.reset(document);
|
|
79
|
+
const present = new Set(elements.map((el) => el.observationIdentity));
|
|
80
|
+
for (const identity of this.anchors.keys()) {
|
|
81
|
+
if (!present.has(identity))
|
|
82
|
+
this.anchors.delete(identity);
|
|
83
|
+
}
|
|
84
|
+
const counts = new Map();
|
|
85
|
+
for (const el of elements) {
|
|
86
|
+
if (el.observationIdentity)
|
|
87
|
+
counts.set(el.observationIdentity, (counts.get(el.observationIdentity) ?? 0) + 1);
|
|
88
|
+
}
|
|
89
|
+
const handles = new Map();
|
|
90
|
+
for (const el of elements) {
|
|
91
|
+
const identity = el.observationIdentity;
|
|
92
|
+
if (!identity || el.observationIntent === undefined || counts.get(identity) !== 1) {
|
|
93
|
+
if (identity)
|
|
94
|
+
this.anchors.delete(identity);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
let anchor = this.anchors.get(identity);
|
|
98
|
+
if (!anchor || anchor.intent !== el.observationIntent) {
|
|
99
|
+
anchor = {
|
|
100
|
+
intent: el.observationIntent,
|
|
101
|
+
ref: this.get(document, `action:${randomBytes(32).toString("base64url")}`),
|
|
102
|
+
};
|
|
103
|
+
this.anchors.set(identity, anchor);
|
|
104
|
+
}
|
|
105
|
+
handles.set(el, anchor.ref);
|
|
106
|
+
}
|
|
107
|
+
return handles;
|
|
108
|
+
}
|
|
109
|
+
/** Compatibility spelling of one capability; never reassign an old alias. */
|
|
110
|
+
label(ref, preferred) {
|
|
111
|
+
const existing = this.aliases.get(ref);
|
|
112
|
+
if (existing !== undefined || preferred === undefined)
|
|
113
|
+
return existing;
|
|
114
|
+
let candidate = preferred;
|
|
115
|
+
for (let suffix = 2; this.aliasOwners.has(candidate); suffix++)
|
|
116
|
+
candidate = labelWithOrdinalV2(preferred, suffix);
|
|
117
|
+
this.aliases.set(ref, candidate);
|
|
118
|
+
this.aliasOwners.set(candidate, ref);
|
|
119
|
+
return candidate;
|
|
120
|
+
}
|
|
121
|
+
constructor(secret = randomBytes(32)) {
|
|
122
|
+
this.secret = secret;
|
|
123
|
+
}
|
|
124
|
+
get(document, identity) {
|
|
125
|
+
this.reset(document);
|
|
126
|
+
let ref = this.refs.get(identity);
|
|
127
|
+
if (ref === undefined) {
|
|
128
|
+
for (let collision = 0;; collision++) {
|
|
129
|
+
const candidate = `@e:${createHmac("sha256", this.secret)
|
|
130
|
+
.update(document)
|
|
131
|
+
.update("\u0000")
|
|
132
|
+
.update(String(this.generation))
|
|
133
|
+
.update("\u0000")
|
|
134
|
+
.update(identity)
|
|
135
|
+
.update("\u0000")
|
|
136
|
+
.update(String(collision))
|
|
137
|
+
.digest("base64url")
|
|
138
|
+
.slice(0, COMPACT_V2_HANDLE_LENGTH)}`;
|
|
139
|
+
const owner = this.identities.get(candidate);
|
|
140
|
+
if (owner === undefined || owner === identity) {
|
|
141
|
+
ref = candidate;
|
|
142
|
+
this.identities.set(ref, identity);
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
this.refs.set(identity, ref);
|
|
147
|
+
}
|
|
148
|
+
return ref;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const LABEL_MAX_CHARS = 32;
|
|
152
|
+
const COMPACT_V2_LABEL_RE = /^@[\p{L}\p{M}\p{N}]+(?:-[\p{L}\p{M}\p{N}]+)*(?:…)?$/u;
|
|
21
153
|
export function isCompactV2Handle(target) {
|
|
22
154
|
return COMPACT_V2_HANDLE_RE.test(target);
|
|
23
155
|
}
|
|
24
156
|
/** The `@label` alias form, distinguishable from a handle by its `@e:` prefix. */
|
|
25
157
|
export function isCompactV2Label(target) {
|
|
26
|
-
return COMPACT_V2_LABEL_RE.test(target)
|
|
158
|
+
return (COMPACT_V2_LABEL_RE.test(target) &&
|
|
159
|
+
/[\p{L}\p{N}]/u.test(target) &&
|
|
160
|
+
Array.from(target.slice(1)).length <= LABEL_MAX_CHARS);
|
|
27
161
|
}
|
|
28
162
|
/**
|
|
29
163
|
* Resolve a handle only when it is well-formed AND a member of the current
|
|
30
|
-
* snapshot's
|
|
164
|
+
* snapshot's action map. Callers must never turn an unknown @e: value into a
|
|
31
165
|
* label or legacy-ref lookup. The handle is document-scoped by construction
|
|
32
|
-
* (the
|
|
166
|
+
* (the allocator retires its document map), so a ref from a replaced document finds no
|
|
33
167
|
* live match rather than resolving onto whatever now occupies its position.
|
|
34
168
|
*/
|
|
35
169
|
export function compactV2LegacyRefForHandle(handles, target) {
|
|
@@ -37,22 +171,92 @@ export function compactV2LegacyRefForHandle(handles, target) {
|
|
|
37
171
|
return null;
|
|
38
172
|
return handles.get(target) ?? null;
|
|
39
173
|
}
|
|
40
|
-
const LABEL_MAX_CHARS = 32;
|
|
41
174
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
175
|
+
* Split a glued "Title + description" accessible name into its leading title,
|
|
176
|
+
* detected from the ORIGINAL name — by slug time the boundary is gone. The
|
|
177
|
+
* 2026-09-06 ipinfo docs dogfood produced labels like
|
|
178
|
+
* `@database-downloadsdownload-ip-da`: two headings (or a heading and its
|
|
179
|
+
* description) concatenated by the DOM with no whitespace, slugified into one
|
|
180
|
+
* unreadable run and hard-cut mid-word. The boundaries that mark that seam in
|
|
181
|
+
* the original name are:
|
|
182
|
+
* - a newline inside the accessible name,
|
|
183
|
+
* - sentence/heading punctuation (`.` `!` `?` `:` `)`) directly abutting the
|
|
184
|
+
* next word (`(Search Ctrl+K)Navigation`, `Docs.Start here`) — a period
|
|
185
|
+
* followed by a lowercase letter ("Node.js") is NOT a boundary,
|
|
186
|
+
* - a lowercase→uppercase case transition where two headings were joined
|
|
187
|
+
* with no space (`DownloadsDownload`).
|
|
188
|
+
* Returns the original name when no qualifying boundary is found, so ordinary
|
|
189
|
+
* multi-word names and short glued pairs keep their full slug.
|
|
45
190
|
*/
|
|
191
|
+
function leadingTitleFromAccessibleName(name) {
|
|
192
|
+
// A candidate title must read as a heading — multi-word, or a substantial
|
|
193
|
+
// standalone word — so camelCase identifiers ("myAccountSettings") and
|
|
194
|
+
// abbreviation fragments ("U.S. Government") keep their full slug, and a
|
|
195
|
+
// real description must remain behind the seam.
|
|
196
|
+
const readsAsHeading = (title) => /[A-Za-z0-9]/.test(title) && (title.includes(" ") || title.length >= 5);
|
|
197
|
+
const hasDescription = (rest) => /[A-Za-z0-9].*[A-Za-z0-9]/.test(rest);
|
|
198
|
+
for (let i = 0; i < name.length - 1; i += 1) {
|
|
199
|
+
const ch = name[i];
|
|
200
|
+
const next = name[i + 1];
|
|
201
|
+
if (ch === "\n") {
|
|
202
|
+
if (readsAsHeading(name.slice(0, i)) && hasDescription(name.slice(i + 1))) {
|
|
203
|
+
return name.slice(0, i);
|
|
204
|
+
}
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (".!?:)".includes(ch) && /[A-Za-z0-9]/.test(next)) {
|
|
208
|
+
// A period followed by a lowercase letter is an abbreviation
|
|
209
|
+
// ("Node.js"), not a heading seam; require an uppercase letter (or a
|
|
210
|
+
// closing parenthesis, which abuts a seam regardless of case).
|
|
211
|
+
if ((ch === ")" || /[A-Z]/.test(next)) && hasDescription(name.slice(i + 1))) {
|
|
212
|
+
const title = name.slice(0, i);
|
|
213
|
+
if (readsAsHeading(title))
|
|
214
|
+
return title;
|
|
215
|
+
}
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (/[a-z]/.test(ch) && /[A-Z]/.test(next)) {
|
|
219
|
+
const title = name.slice(0, i + 1);
|
|
220
|
+
if (readsAsHeading(title) && hasDescription(name.slice(i + 1)))
|
|
221
|
+
return title;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return name;
|
|
225
|
+
}
|
|
226
|
+
/** Addressable slug of the page's control description, with the existing title and length limits. */
|
|
46
227
|
export function controlLabelV2(description) {
|
|
47
228
|
if (description === undefined)
|
|
48
229
|
return undefined;
|
|
49
|
-
const
|
|
230
|
+
const titled = leadingTitleFromAccessibleName(description);
|
|
231
|
+
let slug = titled
|
|
232
|
+
.normalize("NFKC")
|
|
50
233
|
.toLowerCase()
|
|
51
|
-
.replace(/[
|
|
52
|
-
.replace(/^-+|-+$/
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
234
|
+
.replace(/[^\p{L}\p{M}\p{N}]+/gu, "-")
|
|
235
|
+
.replace(/^-+|-+$/gu, "");
|
|
236
|
+
let characters = Array.from(slug);
|
|
237
|
+
const truncated = characters.length > LABEL_MAX_CHARS;
|
|
238
|
+
const usesUnicode = characters.some((character) => character.charCodeAt(0) > 0x7f);
|
|
239
|
+
if (truncated) {
|
|
240
|
+
characters = characters.slice(0, LABEL_MAX_CHARS - Number(usesUnicode));
|
|
241
|
+
// Never cut mid-word: drop the trailing partial segment at the last word
|
|
242
|
+
// boundary. A single unbroken run longer than the budget keeps its
|
|
243
|
+
// mid-run cut — there is no boundary to cut on.
|
|
244
|
+
const lastBoundary = characters.lastIndexOf("-");
|
|
245
|
+
if (lastBoundary > 0)
|
|
246
|
+
characters = characters.slice(0, lastBoundary);
|
|
247
|
+
}
|
|
248
|
+
slug = characters.join("").replace(/-+$/g, "");
|
|
249
|
+
if (slug.length === 0 || !/[\p{L}\p{N}]/u.test(slug))
|
|
250
|
+
return undefined;
|
|
251
|
+
return `@${slug}${truncated && usesUnicode ? "…" : ""}`;
|
|
252
|
+
}
|
|
253
|
+
function labelWithOrdinalV2(label, suffix) {
|
|
254
|
+
const tail = `-${suffix}`;
|
|
255
|
+
const characters = Array.from(label.slice(1));
|
|
256
|
+
const truncated = characters.length > LABEL_MAX_CHARS - Array.from(tail).length || characters.at(-1) === "…";
|
|
257
|
+
const body = truncated ? characters.slice(0, -1) : characters;
|
|
258
|
+
const limit = LABEL_MAX_CHARS - Array.from(tail).length - Number(truncated);
|
|
259
|
+
return `@${body.slice(0, limit).join("")}${tail}${truncated ? "…" : ""}`;
|
|
56
260
|
}
|
|
57
261
|
// The protocol is intentionally positional to keep repeated observes small.
|
|
58
262
|
// Tuple schema: [ref, role(b/l/t/s/c/r/tb/m/f), optional compact description].
|
|
@@ -61,7 +265,7 @@ export function controlLabelV2(description) {
|
|
|
61
265
|
// is deliberately one sparse string rather than nullable columns:
|
|
62
266
|
// checked/unchecked, disabled, action, field, card choice, and frame context
|
|
63
267
|
// remain distinguishable without paying for empty slots on every row. The
|
|
64
|
-
// label
|
|
268
|
+
// label grammar excludes `|` and `=`, so no escaping is needed.
|
|
65
269
|
function wireControl(row) {
|
|
66
270
|
const role = {
|
|
67
271
|
button: "b",
|
|
@@ -293,6 +497,15 @@ export function recordableTokenV2(value) {
|
|
|
293
497
|
* rejects nothing: the truncation is the compactness budget, not redaction.
|
|
294
498
|
*/
|
|
295
499
|
export function safeDescriptionV2(value) {
|
|
500
|
+
const normalized = normalizeDescriptionV2(value);
|
|
501
|
+
if (normalized === undefined)
|
|
502
|
+
return undefined;
|
|
503
|
+
return normalized.length <= SAFE_DESCRIPTION_MAX_CHARS
|
|
504
|
+
? normalized
|
|
505
|
+
: `${normalized.slice(0, SAFE_DESCRIPTION_MAX_CHARS - 1)}…`;
|
|
506
|
+
}
|
|
507
|
+
/** Normalize page descriptions without applying the query row's length limit. */
|
|
508
|
+
export function normalizeDescriptionV2(value) {
|
|
296
509
|
if (typeof value !== "string")
|
|
297
510
|
return undefined;
|
|
298
511
|
// Control characters would break the positional wire encoding; they are not
|
|
@@ -301,11 +514,7 @@ export function safeDescriptionV2(value) {
|
|
|
301
514
|
.replace(/[\p{Cc}\p{Cf}]/gu, " ")
|
|
302
515
|
.replace(/\s+/g, " ")
|
|
303
516
|
.trim();
|
|
304
|
-
|
|
305
|
-
return undefined;
|
|
306
|
-
return normalized.length <= SAFE_DESCRIPTION_MAX_CHARS
|
|
307
|
-
? normalized
|
|
308
|
-
: `${normalized.slice(0, SAFE_DESCRIPTION_MAX_CHARS - 1)}…`;
|
|
517
|
+
return normalized.length === 0 ? undefined : normalized;
|
|
309
518
|
}
|
|
310
519
|
const SAFE_AUTOCOMPLETE_TOKENS = new Set([
|
|
311
520
|
"address-level1",
|
|
@@ -545,7 +754,7 @@ export function sealRetainedInteractiveElementsV2(elements, selectorFor = () =>
|
|
|
545
754
|
export function safePageSemanticsV2(source) {
|
|
546
755
|
const title = safeDescriptionV2(source.title);
|
|
547
756
|
const headings = source.headings
|
|
548
|
-
.map(
|
|
757
|
+
.map(safeDescriptionV2)
|
|
549
758
|
.filter((value) => value !== undefined)
|
|
550
759
|
.filter((value, index, all) => all.indexOf(value) === index)
|
|
551
760
|
.slice(0, 1);
|
|
@@ -554,48 +763,6 @@ export function safePageSemanticsV2(source) {
|
|
|
554
763
|
...(headings.length === 0 ? {} : { headings }),
|
|
555
764
|
};
|
|
556
765
|
}
|
|
557
|
-
/**
|
|
558
|
-
* Diff only the sealed representation. In particular, raw changes to a
|
|
559
|
-
* field value, card number, live region, or merchant copy cannot cause either
|
|
560
|
-
* the raw data or a page-derived derivative to cross into the delta.
|
|
561
|
-
*/
|
|
562
|
-
export function diffSafeControlsV2(previous, stage, rows) {
|
|
563
|
-
const current = new Map(rows.map((row) => [row.ref, row]));
|
|
564
|
-
const added = [];
|
|
565
|
-
const changed = [];
|
|
566
|
-
for (const row of rows) {
|
|
567
|
-
const before = previous.byRef.get(row.ref);
|
|
568
|
-
if (before === undefined)
|
|
569
|
-
added.push(row);
|
|
570
|
-
else if (JSON.stringify(wireControl(before)) !== JSON.stringify(wireControl(row)))
|
|
571
|
-
changed.push(row);
|
|
572
|
-
}
|
|
573
|
-
const removed = [...previous.byRef.keys()].filter((ref) => !current.has(ref));
|
|
574
|
-
return { added, changed, removed, stageChanged: previous.stage !== stage };
|
|
575
|
-
}
|
|
576
|
-
/**
|
|
577
|
-
* Encodes a structural V2 delta. `null` deliberately asks the caller to send
|
|
578
|
-
* a paged full resync instead: a delta must never exceed the hard wire budget.
|
|
579
|
-
*/
|
|
580
|
-
export function encodeV2Delta(args) {
|
|
581
|
-
const payload = {
|
|
582
|
-
format: "compact-v2",
|
|
583
|
-
url: args.pageUrl ?? "",
|
|
584
|
-
text: "",
|
|
585
|
-
session_id: args.sessionId,
|
|
586
|
-
delta: true,
|
|
587
|
-
...(args.semantics === undefined ? {} : { semantic: args.semantics }),
|
|
588
|
-
...(args.delta.stageChanged ? { stage: args.stage } : {}),
|
|
589
|
-
// `safe_table` follows the established TS delta protocol: rows are
|
|
590
|
-
// upserts, irrespective of whether their ref is new or changed. This keeps
|
|
591
|
-
// existing delta consumers compatible while the @e: map itself stays V2.
|
|
592
|
-
...(args.delta.added.length + args.delta.changed.length > 0
|
|
593
|
-
? { safe_table: [...args.delta.added, ...args.delta.changed].map(wireControl) }
|
|
594
|
-
: {}),
|
|
595
|
-
...(args.delta.removed.length > 0 ? { removed: args.delta.removed } : {}),
|
|
596
|
-
};
|
|
597
|
-
return compactV2PayloadWithinBudget(payload) ? payload : null;
|
|
598
|
-
}
|
|
599
766
|
const INTENTS = [
|
|
600
767
|
["add_to_cart", /add\s+(?:to\s+)?(?:cart|bag|basket)/i],
|
|
601
768
|
["view_cart", /view\s+(?:cart|bag|basket)/i],
|
|
@@ -626,6 +793,41 @@ function controlNamingTexts(el) {
|
|
|
626
793
|
isButtonInput(el) ? el.value : undefined,
|
|
627
794
|
];
|
|
628
795
|
}
|
|
796
|
+
function controlLabelNamingTexts(el) {
|
|
797
|
+
const role = roleOf(el);
|
|
798
|
+
const names = el.compactNames;
|
|
799
|
+
if (names) {
|
|
800
|
+
const accessibleName = normalizeDescriptionV2(names.accessibleName);
|
|
801
|
+
const visibleText = normalizeDescriptionV2(names.visibleText);
|
|
802
|
+
const iconLabel = normalizeDescriptionV2(names.iconLabel);
|
|
803
|
+
const aggregateIconName = visibleText !== undefined &&
|
|
804
|
+
iconLabel !== undefined &&
|
|
805
|
+
accessibleName === `${iconLabel} ${visibleText}`;
|
|
806
|
+
return [
|
|
807
|
+
aggregateIconName ? undefined : names.accessibleName,
|
|
808
|
+
names.ariaLabel,
|
|
809
|
+
names.labelledByText,
|
|
810
|
+
names.labelText,
|
|
811
|
+
(el.type ?? "").toLowerCase() === "image" ? names.alt : undefined,
|
|
812
|
+
names.visibleText,
|
|
813
|
+
isButtonInput(el) ? names.value : undefined,
|
|
814
|
+
names.iconLabel,
|
|
815
|
+
names.title,
|
|
816
|
+
role === "textbox" ? names.placeholder : undefined,
|
|
817
|
+
role === "textbox" ? names.name : undefined,
|
|
818
|
+
];
|
|
819
|
+
}
|
|
820
|
+
return [
|
|
821
|
+
el.ariaLabel,
|
|
822
|
+
el.labelText,
|
|
823
|
+
el.visibleText,
|
|
824
|
+
isButtonInput(el) ? el.value : undefined,
|
|
825
|
+
el.iconLabel,
|
|
826
|
+
el.title,
|
|
827
|
+
role === "textbox" ? el.placeholder : undefined,
|
|
828
|
+
role === "textbox" ? el.name : undefined,
|
|
829
|
+
];
|
|
830
|
+
}
|
|
629
831
|
function candidateTexts(el) {
|
|
630
832
|
return [
|
|
631
833
|
el.visibleText,
|
|
@@ -645,12 +847,53 @@ function candidateTexts(el) {
|
|
|
645
847
|
function candidateText(el) {
|
|
646
848
|
return candidateTexts(el).join(" ");
|
|
647
849
|
}
|
|
648
|
-
function controlDescription(el) {
|
|
649
|
-
//
|
|
650
|
-
//
|
|
651
|
-
|
|
652
|
-
|
|
850
|
+
function controlDescription(el, role) {
|
|
851
|
+
// Prefer authored accessibility names over descendant text. Navigation
|
|
852
|
+
// toggles and data-row controls often contain an entire menu/card subtree;
|
|
853
|
+
// choosing textContent first turned their concise aria/label name into a
|
|
854
|
+
// mashed-together alias. Native button values are names, while field values
|
|
855
|
+
// and ids remain excluded. Textbox placeholder/name are useful only after
|
|
856
|
+
// the genuine accessibility and visible-name sources have been exhausted.
|
|
857
|
+
const chosen = controlLabelNamingTexts(el)
|
|
858
|
+
.map((candidate) => normalizeDescriptionV2(candidate))
|
|
653
859
|
.find((candidate) => candidate !== undefined);
|
|
860
|
+
if (chosen !== undefined) {
|
|
861
|
+
// Add the page's region context to otherwise opaque labels (such as @as15169).
|
|
862
|
+
const context = regionContextV2(el.compactNames ? el.compactNames.container : el.container, chosen);
|
|
863
|
+
const description = context === undefined ? chosen : `${chosen} ${context}`;
|
|
864
|
+
if (controlLabelV2(description) !== undefined)
|
|
865
|
+
return description;
|
|
866
|
+
}
|
|
867
|
+
// Every actionable row still needs a human-usable alias. The nearest
|
|
868
|
+
// semantic region is the best available immediate context; a genuinely
|
|
869
|
+
// anonymous control falls back to its role rather than becoming a bare
|
|
870
|
+
// [ref, role] tuple. This is descriptive only and does not alter identity.
|
|
871
|
+
const context = namedContainerContextV2(el.compactNames ? el.compactNames.container : el.container);
|
|
872
|
+
return context === undefined ? `${role} ${el.index + 1}` : `${context} ${role}`;
|
|
873
|
+
}
|
|
874
|
+
/** A description is "uninformative" when it carries no 3+-letter word run: ids, short codes, hex fragments. */
|
|
875
|
+
function isUninformativeDescriptionV2(description) {
|
|
876
|
+
return (!/[a-zA-Z]{3,}/.test(description) &&
|
|
877
|
+
!Array.from(description).some((character) => character.charCodeAt(0) > 0x7f && /\p{L}/u.test(character)));
|
|
878
|
+
}
|
|
879
|
+
function regionContextV2(container, chosen) {
|
|
880
|
+
if (container === null || container === undefined)
|
|
881
|
+
return undefined;
|
|
882
|
+
if (!isUninformativeDescriptionV2(chosen))
|
|
883
|
+
return undefined;
|
|
884
|
+
const context = namedContainerContextV2(container);
|
|
885
|
+
if (context === undefined)
|
|
886
|
+
return undefined;
|
|
887
|
+
return context.slice(0, 24).replace(/-+$/, "") || undefined;
|
|
888
|
+
}
|
|
889
|
+
function namedContainerContextV2(container) {
|
|
890
|
+
if (container === null || container === undefined)
|
|
891
|
+
return undefined;
|
|
892
|
+
const separator = container.indexOf(":");
|
|
893
|
+
const kind = separator < 0 ? undefined : container.slice(0, separator).toLowerCase();
|
|
894
|
+
const raw = separator < 0 ? container : container.slice(separator + 1);
|
|
895
|
+
const context = safeDescriptionV2(raw.replace(/[-_]+/g, " "));
|
|
896
|
+
return context === undefined || context.toLowerCase() === kind ? undefined : context;
|
|
654
897
|
}
|
|
655
898
|
function privateQueryTokenV2(value) {
|
|
656
899
|
const normalized = value.normalize("NFKC").trim().toLowerCase();
|
|
@@ -679,21 +922,40 @@ export function controlMatchesPrivateQueryV2(el, query) {
|
|
|
679
922
|
const needles = privateQueryTermsV2(query);
|
|
680
923
|
if (needles === null)
|
|
681
924
|
return false;
|
|
682
|
-
//
|
|
683
|
-
//
|
|
684
|
-
//
|
|
685
|
-
// — stay queryable
|
|
686
|
-
//
|
|
687
|
-
//
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
925
|
+
// AND-match every needle against the COMBINED token set across all naming
|
|
926
|
+
// sources — visible text, label, aria label, icon, title, placeholder,
|
|
927
|
+
// value, plus name/id slugs (so choice-group members without their own copy
|
|
928
|
+
// — Shopify shipping-rate radios — stay queryable; these are DOM
|
|
929
|
+
// identifiers, never field values) and the control's own role word.
|
|
930
|
+
// Requiring each needle to hit ONE source (the old behavior) made generic
|
|
931
|
+
// multi-term queries — "region dropdown" for a combobox labeled "Region",
|
|
932
|
+
// "use case textbox" for a textarea — return empty on real pages.
|
|
933
|
+
const role = roleOf(el);
|
|
934
|
+
const candidates = [
|
|
935
|
+
...controlNamingTexts(el),
|
|
936
|
+
el.name,
|
|
937
|
+
el.id,
|
|
938
|
+
role,
|
|
939
|
+
];
|
|
940
|
+
const combined = new Set();
|
|
941
|
+
for (const candidate of candidates) {
|
|
942
|
+
if (typeof candidate !== "string" || candidate.length === 0)
|
|
943
|
+
continue;
|
|
691
944
|
const tokens = candidate.normalize("NFKC").match(/[\p{L}\p{M}\p{N}]{1,48}/gu);
|
|
692
945
|
if (tokens === null)
|
|
693
|
-
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
946
|
+
continue;
|
|
947
|
+
for (const token of tokens) {
|
|
948
|
+
const safe = privateQueryTokenV2(token);
|
|
949
|
+
if (safe !== null)
|
|
950
|
+
combined.add(safe);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
if (role === "select") {
|
|
954
|
+
// Common spoken words for the wire role keep role-term queries reachable.
|
|
955
|
+
combined.add("dropdown");
|
|
956
|
+
combined.add("combobox");
|
|
957
|
+
}
|
|
958
|
+
return needles.every((needle) => combined.has(needle));
|
|
697
959
|
}
|
|
698
960
|
function roleOf(el) {
|
|
699
961
|
const role = (el.role ?? "").toLowerCase();
|
|
@@ -894,14 +1156,54 @@ function frameOf(el, pageOrigin) {
|
|
|
894
1156
|
return "main";
|
|
895
1157
|
return el.frameOrigin === pageOrigin ? "same_origin" : "cross_origin";
|
|
896
1158
|
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Deterministically disambiguate duplicate labels within one snapshot. The
|
|
1161
|
+
* /dashboard/token dogfood returned two distinct rows both labelled
|
|
1162
|
+
* `@curl-example` with nothing to tell them apart — a correct pick was a coin
|
|
1163
|
+
* flip. The first occurrence keeps the plain label; the second and later
|
|
1164
|
+
* occurrences get a stable ordinal suffix (`@curl-example-2`, `-3`, …)
|
|
1165
|
+
* following the map's own row order, so the same page always yields the same
|
|
1166
|
+
* numbering. A suffix that would collide with an existing label (the page
|
|
1167
|
+
* itself rendering "curl example 2" alongside two "curl example" rows) is
|
|
1168
|
+
* skipped, so disambiguation never mints a new ambiguity.
|
|
1169
|
+
*/
|
|
1170
|
+
export function disambiguateDuplicateLabelsV2(labels) {
|
|
1171
|
+
const occurrences = new Map();
|
|
1172
|
+
for (const label of labels) {
|
|
1173
|
+
if (label === undefined)
|
|
1174
|
+
continue;
|
|
1175
|
+
occurrences.set(label, (occurrences.get(label) ?? 0) + 1);
|
|
1176
|
+
}
|
|
1177
|
+
const assigned = new Set();
|
|
1178
|
+
for (const label of labels)
|
|
1179
|
+
if (label !== undefined)
|
|
1180
|
+
assigned.add(label);
|
|
1181
|
+
const seen = new Map();
|
|
1182
|
+
return labels.map((label) => {
|
|
1183
|
+
if (label === undefined)
|
|
1184
|
+
return undefined;
|
|
1185
|
+
const occurrence = (seen.get(label) ?? 0) + 1;
|
|
1186
|
+
seen.set(label, occurrence);
|
|
1187
|
+
if (occurrence === 1)
|
|
1188
|
+
return label;
|
|
1189
|
+
let suffix = occurrence;
|
|
1190
|
+
let candidate = labelWithOrdinalV2(label, suffix);
|
|
1191
|
+
while (assigned.has(candidate)) {
|
|
1192
|
+
suffix += 1;
|
|
1193
|
+
candidate = labelWithOrdinalV2(label, suffix);
|
|
1194
|
+
}
|
|
1195
|
+
assigned.add(candidate);
|
|
1196
|
+
return candidate;
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
897
1199
|
export function buildSafeControlsV2(args) {
|
|
898
1200
|
const rows = [];
|
|
899
1201
|
const paymentContext = checkoutStageFromUrlV2(args.pageUrl ?? "") !== null ||
|
|
900
1202
|
args.elements.some((element) => hasExplicitPaymentFieldSignal(element));
|
|
901
1203
|
for (const el of args.elements) {
|
|
902
|
-
if (el.visible !== true || el.topmost === false)
|
|
1204
|
+
if (el.visible !== true || (!args.canonical && el.topmost === false))
|
|
903
1205
|
continue;
|
|
904
|
-
const role = roleOf(el);
|
|
1206
|
+
const role = roleOf(el) ?? (args.canonical ? "button" : null);
|
|
905
1207
|
const legacy = args.legacyRefs.get(el);
|
|
906
1208
|
const ref = args.handles.get(el);
|
|
907
1209
|
if (role === null || legacy === undefined || ref === undefined)
|
|
@@ -914,7 +1216,7 @@ export function buildSafeControlsV2(args) {
|
|
|
914
1216
|
// already CDP-derived interactive inventory supplies each visible control's
|
|
915
1217
|
// descendant/accessibility name. This pass binds that name to its own live
|
|
916
1218
|
// element, so no cross-serializer tag/role fallback can swap labels.
|
|
917
|
-
const label = controlLabelV2(controlDescription(el));
|
|
1219
|
+
const label = controlLabelV2(controlDescription(el, role));
|
|
918
1220
|
const row = {
|
|
919
1221
|
role,
|
|
920
1222
|
visibility: el.inViewport ? "viewport" : "near",
|
|
@@ -939,80 +1241,22 @@ export function buildSafeControlsV2(args) {
|
|
|
939
1241
|
});
|
|
940
1242
|
}
|
|
941
1243
|
rows.sort((a, b) => a.priority - b.priority || a.legacy.localeCompare(b.legacy));
|
|
1244
|
+
// The legacy fallback disambiguates after final ordering. Compact-v2 supplies
|
|
1245
|
+
// anchorLabel instead, reserving each alias for its physical-node capability.
|
|
1246
|
+
const disambiguated = disambiguateDuplicateLabelsV2(rows.map(({ row }) => row.label));
|
|
942
1247
|
const byRef = new Map();
|
|
943
|
-
const safeRows = rows.map(({ ref, legacy, row }) => {
|
|
1248
|
+
const safeRows = rows.map(({ ref, legacy, row }, position) => {
|
|
1249
|
+
const label = args.anchorLabel ? args.anchorLabel(ref, row.label) : disambiguated[position];
|
|
944
1250
|
byRef.set(ref, legacy);
|
|
945
|
-
return { ref, ...row };
|
|
1251
|
+
return { ref, ...row, ...(label === undefined ? {} : { label }) };
|
|
946
1252
|
});
|
|
947
1253
|
return { rows: safeRows, byRef };
|
|
948
1254
|
}
|
|
949
|
-
export function
|
|
1255
|
+
export function encodeV2QueryPage(args) {
|
|
950
1256
|
const offset = args.offset ?? 0;
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
format: "compact-v2",
|
|
954
|
-
url: args.pageUrl ?? "",
|
|
955
|
-
text: "",
|
|
956
|
-
session_id: args.sessionId,
|
|
957
|
-
stage: args.stage,
|
|
958
|
-
...(args.startMetadata?.hint === undefined ? {} : { hint: args.startMetadata.hint }),
|
|
959
|
-
...(args.startMetadata?.userEmail === undefined
|
|
960
|
-
? {}
|
|
961
|
-
: { user_email: args.startMetadata.userEmail }),
|
|
962
|
-
...(args.startMetadata?.hintOverflow === undefined
|
|
963
|
-
? {}
|
|
964
|
-
: { hint_overflow: args.startMetadata.hintOverflow }),
|
|
965
|
-
...(args.semantics === undefined || Object.keys(args.semantics).length === 0
|
|
966
|
-
? {}
|
|
967
|
-
: { semantic: args.semantics }),
|
|
968
|
-
delta: true,
|
|
969
|
-
};
|
|
970
|
-
if (!compactV2PayloadWithinBudget(payload)) {
|
|
971
|
-
throw new Error("compact-v2 budget metadata exceeded");
|
|
972
|
-
}
|
|
973
|
-
return {
|
|
974
|
-
payload,
|
|
975
|
-
nextOffset: 0,
|
|
976
|
-
};
|
|
977
|
-
}
|
|
978
|
-
const visible = [];
|
|
979
|
-
// Initial observations stay decisively small. Overflow is retrieved through
|
|
980
|
-
// the in-MCP query cursor, never a shell-readable snapshot file.
|
|
981
|
-
const pageEnd = offset === 0 ? Math.min(args.rows.length, FIRST_PAGE_ROW_LIMIT) : args.rows.length;
|
|
982
|
-
for (let index = offset; index < pageEnd; index += 1) {
|
|
983
|
-
const candidate = args.rows[index];
|
|
984
|
-
const remainder = args.rows.length - (index + 1);
|
|
985
|
-
const payload = {
|
|
986
|
-
format: "compact-v2",
|
|
987
|
-
url: args.pageUrl ?? "",
|
|
988
|
-
text: "",
|
|
989
|
-
session_id: args.sessionId,
|
|
990
|
-
stage: args.stage,
|
|
991
|
-
...(args.startMetadata?.hint === undefined ? {} : { hint: args.startMetadata.hint }),
|
|
992
|
-
...(args.startMetadata?.userEmail === undefined
|
|
993
|
-
? {}
|
|
994
|
-
: { user_email: args.startMetadata.userEmail }),
|
|
995
|
-
...(args.startMetadata?.hintOverflow === undefined
|
|
996
|
-
? {}
|
|
997
|
-
: { hint_overflow: args.startMetadata.hintOverflow }),
|
|
998
|
-
...(args.semantics === undefined || Object.keys(args.semantics).length === 0
|
|
999
|
-
? {}
|
|
1000
|
-
: { semantic: args.semantics }),
|
|
1001
|
-
safe_table: [...visible, wireControl(candidate)],
|
|
1002
|
-
...(remainder > 0
|
|
1003
|
-
? { overflow: { remaining: remainder, next_cursor: args.cursorFor(index + 1) } }
|
|
1004
|
-
: {}),
|
|
1005
|
-
};
|
|
1006
|
-
if (!compactV2PayloadWithinBudget(payload))
|
|
1007
|
-
break;
|
|
1008
|
-
visible.push(wireControl(candidate));
|
|
1009
|
-
}
|
|
1010
|
-
const nextOffset = offset + visible.length;
|
|
1011
|
-
const remaining = args.rows.length - nextOffset;
|
|
1012
|
-
const payload = {
|
|
1013
|
-
format: "compact-v2",
|
|
1257
|
+
let fixed = {
|
|
1258
|
+
format: "browser-use-control-query",
|
|
1014
1259
|
url: args.pageUrl ?? "",
|
|
1015
|
-
text: "",
|
|
1016
1260
|
session_id: args.sessionId,
|
|
1017
1261
|
stage: args.stage,
|
|
1018
1262
|
...(args.startMetadata?.hint === undefined ? {} : { hint: args.startMetadata.hint }),
|
|
@@ -1025,13 +1269,58 @@ export function encodeV2Page(args) {
|
|
|
1025
1269
|
...(args.semantics === undefined || Object.keys(args.semantics).length === 0
|
|
1026
1270
|
? {}
|
|
1027
1271
|
: { semantic: args.semantics }),
|
|
1028
|
-
safe_table: visible,
|
|
1029
|
-
...(remaining > 0 ? { overflow: { remaining, next_cursor: args.cursorFor(nextOffset) } } : {}),
|
|
1030
1272
|
};
|
|
1031
|
-
|
|
1032
|
-
|
|
1273
|
+
const pageWith = (table, nextOffset) => {
|
|
1274
|
+
const remaining = args.rows.length - nextOffset;
|
|
1275
|
+
return {
|
|
1276
|
+
...fixed,
|
|
1277
|
+
safe_table: table,
|
|
1278
|
+
...(remaining > 0
|
|
1279
|
+
? { overflow: { remaining, next_cursor: args.cursorFor(nextOffset) } }
|
|
1280
|
+
: {}),
|
|
1281
|
+
};
|
|
1282
|
+
};
|
|
1283
|
+
if (offset < args.rows.length) {
|
|
1284
|
+
const probe = pageWith([wireControl(args.rows[offset])], offset + 1);
|
|
1285
|
+
if (!compactV2PayloadWithinBudget(probe)) {
|
|
1286
|
+
const degraded = compactV2DegradeMetadata(probe);
|
|
1287
|
+
if (degraded === null)
|
|
1288
|
+
throw new Error("compact-v2 budget metadata exceeded");
|
|
1289
|
+
const degradedFixed = { ...degraded };
|
|
1290
|
+
delete degradedFixed.safe_table;
|
|
1291
|
+
delete degradedFixed.overflow;
|
|
1292
|
+
fixed = degradedFixed;
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
const visible = [];
|
|
1296
|
+
// Every interactive control is a candidate for the default map: rows are
|
|
1297
|
+
// packed in priority order until the wire budget is actually reached, so a
|
|
1298
|
+
// below-the-fold primary CTA is never stranded behind an arbitrary row cap
|
|
1299
|
+
// (the live Xata failure). Overflow remains reachable through the in-MCP
|
|
1300
|
+
// query cursor, never a shell-readable snapshot file.
|
|
1301
|
+
let nextOffset = offset;
|
|
1302
|
+
for (let index = offset; index < args.rows.length; index += 1) {
|
|
1303
|
+
const candidate = wireControl(args.rows[index]);
|
|
1304
|
+
if (!compactV2PayloadWithinBudget(pageWith([...visible, candidate], index + 1)))
|
|
1305
|
+
break;
|
|
1306
|
+
visible.push(candidate);
|
|
1307
|
+
nextOffset = index + 1;
|
|
1308
|
+
}
|
|
1309
|
+
if (visible.length === 0 && offset < args.rows.length) {
|
|
1310
|
+
// Budget-driven packing must never emit an empty page whose cursor points
|
|
1311
|
+
// back at the same offset. Force the first row in and let fixed metadata
|
|
1312
|
+
// degrade around it — on a real page this is unreachable.
|
|
1313
|
+
visible.push(wireControl(args.rows[offset]));
|
|
1314
|
+
nextOffset = offset + 1;
|
|
1315
|
+
}
|
|
1316
|
+
let payload = pageWith(visible, nextOffset);
|
|
1033
1317
|
if (!compactV2PayloadWithinBudget(payload)) {
|
|
1034
|
-
|
|
1318
|
+
// Only hostile fixed metadata (a code-owned session id/cursor) lands here;
|
|
1319
|
+
// degrade the metadata before ever dropping an actionable row.
|
|
1320
|
+
const degraded = compactV2DegradeMetadata(payload);
|
|
1321
|
+
if (degraded === null)
|
|
1322
|
+
throw new Error("compact-v2 budget metadata exceeded");
|
|
1323
|
+
payload = degraded;
|
|
1035
1324
|
}
|
|
1036
1325
|
return { payload, nextOffset };
|
|
1037
1326
|
}
|