browser-broker 0.1.0 → 0.2.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/.env.example +6 -5
- package/README.md +59 -19
- package/RELEASES.md +255 -97
- package/dist/package.json +3 -2
- package/dist/src/adapter/conformance/service-subject.js +5 -1
- package/dist/src/browser/fake.js +16 -2
- package/dist/src/browser/real.js +10 -2
- package/dist/src/cli/adapter.js +37 -3
- package/dist/src/cli/commands.js +37 -1
- package/dist/src/cli/index.js +12 -1
- package/dist/src/cli/operations-commands.js +82 -4
- package/dist/src/cli/reconcile-command.js +35 -3
- package/dist/src/doctor/checks.js +64 -0
- package/dist/src/doctor/report.js +10 -1
- package/dist/src/service/arbitration.js +61 -0
- package/dist/src/service/bridge.js +148 -2
- package/dist/src/service/broker.js +52 -1
- package/dist/src/service/browser-session.js +66 -0
- package/dist/src/service/operations/claim.js +63 -0
- package/dist/src/service/operations/pages.js +66 -4
- package/dist/src/service/operations/status.js +8 -0
- package/dist/src/service/pages.js +81 -0
- package/dist/src/service/reconcile.js +75 -3
- package/dist/src/service/runtime.js +26 -1
- package/dist/src/service/tabs.js +70 -0
- package/dist/src/tool/session.js +17 -4
- package/dist/src/tool/tools.js +154 -3
- package/package.json +3 -2
|
@@ -82,7 +82,9 @@
|
|
|
82
82
|
* on the assumption the seam holds up its end.
|
|
83
83
|
*/
|
|
84
84
|
export function decideReconciliation(pages, recorded) {
|
|
85
|
-
const skippedOpening = recorded
|
|
85
|
+
const skippedOpening = recorded
|
|
86
|
+
.filter((tab) => tab.driverTabId === null)
|
|
87
|
+
.map((tab) => ({ tabId: tab.tabId, sessionId: tab.sessionId }));
|
|
86
88
|
// Every driver name a live lease claims. Built from the rows that have one,
|
|
87
89
|
// which by §1.4's check is exactly the rows that are not `opening`.
|
|
88
90
|
const owned = new Set(recorded
|
|
@@ -123,7 +125,8 @@ export function readRecordedTabs(db, browserId) {
|
|
|
123
125
|
return db
|
|
124
126
|
.prepare(`SELECT tabs.id AS tabId,
|
|
125
127
|
tabs.driver_tab_id AS driverTabId,
|
|
126
|
-
tabs.claim_id AS claimId
|
|
128
|
+
tabs.claim_id AS claimId,
|
|
129
|
+
claims.session_id AS sessionId
|
|
127
130
|
FROM tabs
|
|
128
131
|
JOIN claims ON claims.id = tabs.claim_id
|
|
129
132
|
WHERE tabs.browser_id = ?
|
|
@@ -132,6 +135,57 @@ export function readRecordedTabs(db, browserId) {
|
|
|
132
135
|
ORDER BY tabs.id`)
|
|
133
136
|
.all(browserId);
|
|
134
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Settle rows stranded at `closing` whose page the browser does not have.
|
|
140
|
+
*
|
|
141
|
+
* ── Why the vanished-tab path cannot reach these ────────────────────────
|
|
142
|
+
*
|
|
143
|
+
* {@link readRecordedTabs} requires `claims.state = 'active'`, and rightly:
|
|
144
|
+
* a lapsed lease's rows are the sweep's business, not reconciliation's. But
|
|
145
|
+
* that is exactly the population that strands. A lease ends, its tab is moved
|
|
146
|
+
* to `closing`, and if the answer is never written back the row is left in a
|
|
147
|
+
* state meaning "waiting for the tool" — attached to a lease that is no
|
|
148
|
+
* longer active, and therefore invisible to every later reconciliation.
|
|
149
|
+
*
|
|
150
|
+
* A store was found holding 22 such rows, the oldest two days old, every one
|
|
151
|
+
* with `close_attempts = 0`. The pages had been closed by hand; the rows
|
|
152
|
+
* could not be reached by anything.
|
|
153
|
+
*
|
|
154
|
+
* ── Why this is safe to settle without asking again ─────────────────────
|
|
155
|
+
*
|
|
156
|
+
* The caller has just asked the browser what it has open, and passes the
|
|
157
|
+
* driver names it answered with. A row whose name is not in that list
|
|
158
|
+
* describes a page this browser does not have, so there is no round trip
|
|
159
|
+
* outstanding and nothing to wait for — the same reasoning
|
|
160
|
+
* {@link applyReconciliation} uses for a vanished page, and the sweep uses
|
|
161
|
+
* for a tab that never opened.
|
|
162
|
+
*
|
|
163
|
+
* **A row whose name IS in the list is left alone.** Its page exists, the
|
|
164
|
+
* close may genuinely still be in flight, and settling it would claim an
|
|
165
|
+
* answer nobody has given.
|
|
166
|
+
*/
|
|
167
|
+
export function settleStrandedTabs(db, browserId, openDriverTabIds, at) {
|
|
168
|
+
const stranded = db
|
|
169
|
+
.prepare(`SELECT tabs.id AS tabId, tabs.driver_tab_id AS driverTabId
|
|
170
|
+
FROM tabs
|
|
171
|
+
JOIN claims ON claims.id = tabs.claim_id
|
|
172
|
+
WHERE tabs.browser_id = ?
|
|
173
|
+
AND tabs.state = 'closing'
|
|
174
|
+
AND claims.state <> 'active'
|
|
175
|
+
ORDER BY tabs.id`)
|
|
176
|
+
.all(browserId);
|
|
177
|
+
const open = new Set(openDriverTabIds);
|
|
178
|
+
const gone = stranded.filter((tab) => tab.driverTabId === null || !open.has(tab.driverTabId));
|
|
179
|
+
if (gone.length === 0) {
|
|
180
|
+
return 0;
|
|
181
|
+
}
|
|
182
|
+
const placeholders = gone.map(() => '?').join(', ');
|
|
183
|
+
db.prepare(`UPDATE tabs
|
|
184
|
+
SET state = 'closed', closed_at = ?, updated_at = ?
|
|
185
|
+
WHERE id IN (${placeholders})
|
|
186
|
+
AND state = 'closing'`).run(at, at, ...gone.map((tab) => tab.tabId));
|
|
187
|
+
return gone.length;
|
|
188
|
+
}
|
|
135
189
|
/**
|
|
136
190
|
* Settle the rows whose pages are gone, and end the leases that held them.
|
|
137
191
|
*
|
|
@@ -155,6 +209,24 @@ export function readRecordedTabs(db, browserId) {
|
|
|
155
209
|
* | `updateSweptTabs` | The lease ended; is there a page to close? | `closing` — the tool is about to be asked |
|
|
156
210
|
* | this | The page is already gone | `closed` — there is nothing to ask |
|
|
157
211
|
*
|
|
212
|
+
* **`closing` appears in the predicate, and no row reaches it in that
|
|
213
|
+
* state.** The rows here come from {@link readRecordedTabs} by way of
|
|
214
|
+
* {@link decideReconciliation}, and that read requires `tabs.state IN
|
|
215
|
+
* ('opening', 'open')` — so the third value in the predicate below matches
|
|
216
|
+
* nothing this caller can supply. It is kept as a bound on what the write is
|
|
217
|
+
* permitted to touch rather than as a population it serves: the statement
|
|
218
|
+
* says which states this function may move a row out of, and a future caller
|
|
219
|
+
* that widens its own read cannot silently reopen a `closed` row through it.
|
|
220
|
+
*
|
|
221
|
+
* **The stranded-`closing` population is {@link settleStrandedTabs}'s**, not
|
|
222
|
+
* this function's, and the distinction is load-bearing. Those rows belong to
|
|
223
|
+
* leases that have already ended, which is precisely why `readRecordedTabs`
|
|
224
|
+
* (`claims.state = 'active'`) cannot see them and why they need their own
|
|
225
|
+
* pass. A store was found holding 22 of them, each still occupying its slot
|
|
226
|
+
* in the partial unique index on `(browser_id, driver_tab_id)`. Reading that
|
|
227
|
+
* story as this function's would leave the impression the gap is covered
|
|
228
|
+
* here, and it is not.
|
|
229
|
+
*
|
|
158
230
|
* A vanished page has no round trip outstanding, so `closing` would assert
|
|
159
231
|
* one that is not, and the row would wait forever for an answer nobody is
|
|
160
232
|
* coming to give — the exact reasoning the sweep uses for a tab that never
|
|
@@ -193,7 +265,7 @@ export function applyReconciliation(db, vanished, at) {
|
|
|
193
265
|
db.prepare(`UPDATE tabs
|
|
194
266
|
SET state = 'closed', closed_at = ?, updated_at = ?
|
|
195
267
|
WHERE id IN (${tabPlaceholders})
|
|
196
|
-
AND state IN ('opening', 'open')`).run(at, at, ...tabIds);
|
|
268
|
+
AND state IN ('opening', 'open', 'closing')`).run(at, at, ...tabIds);
|
|
197
269
|
// The lease goes with the tab, because a lease *is* a tab (§2.3): a lease
|
|
198
270
|
// whose tab is gone owns nothing while still counting against the budget,
|
|
199
271
|
// which §3.13 names as a state that should not exist.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { recordTabCloseFailed, recordTabClosed } from "./arbitration.js";
|
|
1
2
|
import { readEnvironment } from "../config/environment.js";
|
|
2
3
|
import { prepareStore } from "../store/open.js";
|
|
3
4
|
import { ArtifactStore } from "../artifacts/store.js";
|
|
@@ -70,6 +71,14 @@ export async function createRuntime(options) {
|
|
|
70
71
|
environment,
|
|
71
72
|
adapter: options.adapter,
|
|
72
73
|
session: browsers.session,
|
|
74
|
+
// **What makes `status` able to tell the truth about a dead browser.**
|
|
75
|
+
// Supplied here rather than defaulted inside the broker for the reason
|
|
76
|
+
// the option's own comment gives: a build that cannot look must report
|
|
77
|
+
// `unknown` rather than claim the browser is fine. This build can look,
|
|
78
|
+
// so it does. Note this is the provider's `liveness`, which asks the
|
|
79
|
+
// operating system — not its memoised session, which is the very thing
|
|
80
|
+
// that keeps presenting a dead browser as a working connection.
|
|
81
|
+
checkBrowser: browsers.liveness,
|
|
73
82
|
artifacts,
|
|
74
83
|
// The same provider closes the tabs the sweep orphaned. Without one,
|
|
75
84
|
// `SCHEMA.md` §2.4b's "a leaked tab is not a leaked lease" describes a
|
|
@@ -79,8 +88,24 @@ export async function createRuntime(options) {
|
|
|
79
88
|
closeTab: async (tab) => {
|
|
80
89
|
const session = await browsers.session(tab.browserId);
|
|
81
90
|
const opened = await resolveDriverTab(store.db, tab.tabId);
|
|
82
|
-
if (opened
|
|
91
|
+
if (opened === undefined) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// **The answer is written down either way.** `closing` means "the tool
|
|
95
|
+
// was asked and has not answered", so a close that returns and is never
|
|
96
|
+
// recorded leaves a row saying that forever — which is what happened,
|
|
97
|
+
// 22 rows deep, until a person noticed his browser had filled with
|
|
98
|
+
// pages no lease owned.
|
|
99
|
+
//
|
|
100
|
+
// A failure is recorded rather than thrown: §2.4b's "a leaked tab is
|
|
101
|
+
// not a leaked lease" means the capacity is already back, and failing
|
|
102
|
+
// the release over the page would fail a call that did its job.
|
|
103
|
+
try {
|
|
83
104
|
await session.closeTab({ browser: tab.browserId, driverTabId: opened });
|
|
105
|
+
recordTabClosed(store.db, tab.tabId, new Date().toISOString());
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
recordTabCloseFailed(store.db, tab.tabId, new Date().toISOString());
|
|
84
109
|
}
|
|
85
110
|
},
|
|
86
111
|
});
|
package/dist/src/service/tabs.js
CHANGED
|
@@ -121,3 +121,73 @@ export function recordTabOpened(db, tabId, driverTabId) {
|
|
|
121
121
|
throw new Error(`Tab ${tabId} was not awaiting an open, so the driver name could not be recorded against it.`);
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* How many tabs on one browser are waiting on a close that will not come.
|
|
126
|
+
*
|
|
127
|
+
* ── Why this is one function and not two ────────────────────────────────
|
|
128
|
+
*
|
|
129
|
+
* `broker doctor` and the claim grant both have to answer *"is this browser
|
|
130
|
+
* carrying a backlog of stranded tabs"*, and a product holding two notions of
|
|
131
|
+
* "stranded" would be the same defect the backlog itself caused: a caller
|
|
132
|
+
* told one number by one surface and a different number by another has no way
|
|
133
|
+
* to tell which is the real one. So the predicate lives here once, and both
|
|
134
|
+
* read it.
|
|
135
|
+
*
|
|
136
|
+
* ── Why the threshold is a lease's own lifetime ─────────────────────────
|
|
137
|
+
*
|
|
138
|
+
* Taken from `doctor`'s existing definition rather than invented alongside
|
|
139
|
+
* it. `closing` means the tool was asked and has not answered — a transient
|
|
140
|
+
* state measured in a round trip. The honest boundary between "a close is in
|
|
141
|
+
* flight" and "a close is never happening" is the one the system already uses
|
|
142
|
+
* to decide a caller is gone: if a lease may be declared lapsed after this
|
|
143
|
+
* long without contact, a round trip outstanding for longer is not pending.
|
|
144
|
+
*
|
|
145
|
+
* The comparison is on `updated_at`, which is when the row was moved to
|
|
146
|
+
* `closing`. A round trip still inside the window is deliberately not
|
|
147
|
+
* counted, because reporting one would make a healthy release look like a
|
|
148
|
+
* fault.
|
|
149
|
+
*/
|
|
150
|
+
export function countStrandedTabsFor(db, browserId, leaseSeconds, at = new Date()) {
|
|
151
|
+
const cutoff = new Date(at.getTime() - leaseSeconds * 1000).toISOString();
|
|
152
|
+
const row = db
|
|
153
|
+
.prepare(`SELECT COUNT(*) AS n
|
|
154
|
+
FROM tabs
|
|
155
|
+
WHERE browser_id = ?
|
|
156
|
+
AND state = 'closing'
|
|
157
|
+
AND updated_at < ?`)
|
|
158
|
+
.get(browserId, cutoff);
|
|
159
|
+
return row?.n ?? 0;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The stranded backlog broken down per browser, heaviest first.
|
|
163
|
+
*
|
|
164
|
+
* ── Why a breakdown rather than a total ─────────────────────────────────
|
|
165
|
+
*
|
|
166
|
+
* A total is not actionable when the remedy is per-browser. `doctor`'s
|
|
167
|
+
* remedy line says to run `broker reconcile` against each browser, but a
|
|
168
|
+
* single total cannot say which ones still need it: an operator who
|
|
169
|
+
* reconciled `regular` and saw the count fall from 29 to 13 reasonably
|
|
170
|
+
* concluded reconcile had not worked, when in fact the remaining 13 were all
|
|
171
|
+
* on `private` and the run had done exactly what it said.
|
|
172
|
+
*
|
|
173
|
+
* **Grouped rather than asked per browser from a configured list**, because
|
|
174
|
+
* the browsers are a configured list per kind and not a fixed pair — a
|
|
175
|
+
* breakdown assembled from the list this build happens to know about would
|
|
176
|
+
* silently omit a backlog on a browser that had been reconfigured away, which
|
|
177
|
+
* is the population most likely to be stranded.
|
|
178
|
+
*
|
|
179
|
+
* Only browsers carrying a backlog appear. A row reading zero is not a
|
|
180
|
+
* finding, and listing every configured browser on every healthy run would
|
|
181
|
+
* bury the one line that matters.
|
|
182
|
+
*/
|
|
183
|
+
export function strandedTabsByBrowser(db, leaseSeconds, at = new Date()) {
|
|
184
|
+
const cutoff = new Date(at.getTime() - leaseSeconds * 1000).toISOString();
|
|
185
|
+
return db
|
|
186
|
+
.prepare(`SELECT browser_id AS browserId, COUNT(*) AS stranded
|
|
187
|
+
FROM tabs
|
|
188
|
+
WHERE state = 'closing'
|
|
189
|
+
AND updated_at < ?
|
|
190
|
+
GROUP BY browser_id
|
|
191
|
+
ORDER BY COUNT(*) DESC, browser_id`)
|
|
192
|
+
.all(cutoff);
|
|
193
|
+
}
|
package/dist/src/tool/session.js
CHANGED
|
@@ -14,13 +14,26 @@ import { TOOLS_BY_NAME, TOOL_DEFINITIONS } from "./tools.js";
|
|
|
14
14
|
*
|
|
15
15
|
* Read once at module load rather than per handshake: it cannot change while
|
|
16
16
|
* the process runs, and a session serves one client.
|
|
17
|
+
*
|
|
18
|
+
* ── Why the read is allowed to fail ─────────────────────────────────────
|
|
19
|
+
*
|
|
20
|
+
* A missing or unreadable manifest must not take the surface down with it.
|
|
21
|
+
* This module is imported by the executable a client spawns, so a throw here
|
|
22
|
+
* happens *before* anything can be served and before the protocol exists to
|
|
23
|
+
* describe it — the client sees the process exit, not an error it can act on.
|
|
24
|
+
* Weighed against that, the cost of not knowing the version is a cosmetic
|
|
25
|
+
* field in one handshake.
|
|
26
|
+
*
|
|
27
|
+
* The command line degrades this way already, and by accident rather than by
|
|
28
|
+
* design: it reads the manifest inside the function behind `--version`, so
|
|
29
|
+
* every other command keeps working when the file is gone. Matching that
|
|
30
|
+
* behaviour deliberately is the point of the `catch` — the fallback is the
|
|
31
|
+
* same literal this constant held before it read anything.
|
|
17
32
|
*/
|
|
18
|
-
const manifest = await import('../../package.json', { with: { type: 'json' } });
|
|
33
|
+
const manifest = await import('../../package.json', { with: { type: 'json' } }).catch(() => undefined);
|
|
19
34
|
export const SERVER_INFO = {
|
|
20
35
|
name: 'browser-broker',
|
|
21
|
-
version: typeof manifest
|
|
22
|
-
? manifest.default.version
|
|
23
|
-
: '0.0.0',
|
|
36
|
+
version: typeof manifest?.default?.version === 'string' ? manifest.default.version : '0.0.0',
|
|
24
37
|
};
|
|
25
38
|
/**
|
|
26
39
|
* What `initialize` returns: the negotiated revision, what this server can
|
package/dist/src/tool/tools.js
CHANGED
|
@@ -1,5 +1,136 @@
|
|
|
1
1
|
import { OPERATION_NAMES } from "../adapter/operations.js";
|
|
2
2
|
import { BROWSER_CHOICE_GUIDANCE } from "../browser/driver.js";
|
|
3
|
+
/**
|
|
4
|
+
* The twelve tools, their descriptions, and their argument schemas.
|
|
5
|
+
*
|
|
6
|
+
* ── Surface area is a standing tax, and this file is where it is paid ────
|
|
7
|
+
*
|
|
8
|
+
* `SCHEMA.md` §3.1 opens with it: every description here sits in a connected
|
|
9
|
+
* session's context **on every turn**, whether or not anything calls the
|
|
10
|
+
* tool. Twelve descriptions is the whole agent-facing documentation of this
|
|
11
|
+
* service and it is also a per-turn cost on every session, so each one is
|
|
12
|
+
* written to be the shortest text that still prevents a wrong call.
|
|
13
|
+
*
|
|
14
|
+
* **The description is the only place a calling agent reliably reads.** Not
|
|
15
|
+
* `SCHEMA.md`, not a wiki, not a refusal it has not hit yet. So where a fact
|
|
16
|
+
* changes what a caller does — that `browser_status` is also the renew verb,
|
|
17
|
+
* that the browser choice has no default, that feedback needs no lease — the
|
|
18
|
+
* fact is in the description rather than only in the argument list.
|
|
19
|
+
*
|
|
20
|
+
* **`browser_sign_in`'s description is the strongest case of that rule, and
|
|
21
|
+
* the reason it reads as an instruction rather than a summary.** The failure
|
|
22
|
+
* it exists to end is a caller hitting a login wall and never learning it
|
|
23
|
+
* could ask — and **no refusal can reach that caller**, because it never
|
|
24
|
+
* makes a call to be refused. It abandons the task or fabricates a session
|
|
25
|
+
* instead, which is what 25 measured sessions did in a month. There is no
|
|
26
|
+
* second surface for that guidance to live on, so the description names the
|
|
27
|
+
* alternative outright, the way a refusal would.
|
|
28
|
+
*
|
|
29
|
+
* ── What is deliberately absent ─────────────────────────────────────────
|
|
30
|
+
*
|
|
31
|
+
* **There is no browser-scoped destructive verb, and there must never be
|
|
32
|
+
* one.** No close-browser, no kill-all, no restart. `SCHEMA.md` §3.13 makes
|
|
33
|
+
* that part of the contract rather than an omission: the administrative
|
|
34
|
+
* operations act on something every caller shares, so they are commands a
|
|
35
|
+
* person runs (§4.3, §5.4) and the ledger records that a person did.
|
|
36
|
+
* **The worst thing an agent can do through this surface is close its own
|
|
37
|
+
* tab**, and that ceiling is the reason this surface can be handed to an
|
|
38
|
+
* arbitrary caller at all.
|
|
39
|
+
*
|
|
40
|
+
* **`browser_sign_in` and `browser_sign_in_done` move a browser's state and
|
|
41
|
+
* do not breach that ceiling**, which is worth stating because they look like
|
|
42
|
+
* the exception. Both are keyed, and both act only on the lease that called
|
|
43
|
+
* them: the first takes the browser only when no *other* lease holds a tab on
|
|
44
|
+
* it, and the second is refused unless the calling lease is the one that
|
|
45
|
+
* asked — so a caller can neither interrupt somebody else's work nor end a
|
|
46
|
+
* person's sign-in command mid-password. The unkeyed pair that could do those
|
|
47
|
+
* things, `begin_sign_in` and `end_sign_in`, is deliberately not on this
|
|
48
|
+
* surface at all.
|
|
49
|
+
*
|
|
50
|
+
* `browser_tab_close` is absent for a second, separate reason (§3.1): it
|
|
51
|
+
* closed a caller's only tab while keeping the lease, producing a lease that
|
|
52
|
+
* owned nothing and still consumed budget. It is gone rather than deprecated,
|
|
53
|
+
* and it should not be reintroduced.
|
|
54
|
+
*/
|
|
55
|
+
/**
|
|
56
|
+
* What a capture can and cannot be trusted to show (§3.11).
|
|
57
|
+
*
|
|
58
|
+
* ── A picture is the one result a caller cannot check against itself ─────
|
|
59
|
+
*
|
|
60
|
+
* Every other operation here returns something self-describing: a claim
|
|
61
|
+
* returns a key, an evaluation returns a value, a refusal explains itself. A
|
|
62
|
+
* capture returns a path to an image, and **an image of a half-rendered page
|
|
63
|
+
* looks exactly like an image of a broken one.** There is no field in the
|
|
64
|
+
* result to distrust, so a caller has nothing to weigh the picture against —
|
|
65
|
+
* which makes a wrong conclusion from a capture both easy to reach and hard
|
|
66
|
+
* to notice.
|
|
67
|
+
*
|
|
68
|
+
* The consequence is not hypothetical, and it is what puts this text on the
|
|
69
|
+
* description rather than in a document: a field session captured a canvas
|
|
70
|
+
* before it had drawn, read the dark frame as the page's real appearance, and
|
|
71
|
+
* **reported a fault against an application that did not have one.** It was
|
|
72
|
+
* caught only because a second measurement happened to disagree — which is
|
|
73
|
+
* not a mechanism anything can rely on.
|
|
74
|
+
*
|
|
75
|
+
* ── Why settling does not already cover this ─────────────────────────────
|
|
76
|
+
*
|
|
77
|
+
* Every capture settles the page first, and settling is worth exactly what
|
|
78
|
+
* §3.11 claims: it stops animations and transitions, hides the caret, and
|
|
79
|
+
* waits for web fonts, so the same page yields the same pixels run to run.
|
|
80
|
+
* **That is repeatability, not completeness.** It makes a moving page hold
|
|
81
|
+
* still; it cannot make an unfinished page finish. A canvas that has not
|
|
82
|
+
* drawn its first frame is not moving — it is absent — and holding it still
|
|
83
|
+
* is not the same as waiting for it.
|
|
84
|
+
*
|
|
85
|
+
* ── Why this names a check rather than a duration ────────────────────────
|
|
86
|
+
*
|
|
87
|
+
* The tempting sentence is *"allow the page to settle first"*, and it is
|
|
88
|
+
* advice a caller cannot act on: **the right wait is a property of the page,
|
|
89
|
+
* not of this service**, and a caller that has never seen the page rendered
|
|
90
|
+
* has no way to pick a number. Any figure written here would be wrong for
|
|
91
|
+
* some page and would be trusted anyway, which is worse than saying nothing.
|
|
92
|
+
*
|
|
93
|
+
* So the guidance is a **check** instead — capture twice and compare, which
|
|
94
|
+
* this tool can already express through `compare_to`. A caller can act on it
|
|
95
|
+
* without knowing anything about the page in advance, and it answers the
|
|
96
|
+
* question actually being asked, which is not *"has enough time passed"* but
|
|
97
|
+
* *"has this stopped changing"*. Two identical frames are evidence; one frame
|
|
98
|
+
* and a duration are an assumption.
|
|
99
|
+
*/
|
|
100
|
+
const CAPTURE_SETTLE_CAVEAT = 'Captures are settled — animations stopped, fonts waited for — so a page yields the same ' +
|
|
101
|
+
'pixels twice; that steadies a moving page but does not wait for one still drawing. A canvas ' +
|
|
102
|
+
'or a deferred region can be captured before it has rendered, and the picture will look like a ' +
|
|
103
|
+
'broken page rather than an early one. When a frame looks wrong, capture again with compare_to ' +
|
|
104
|
+
'and check it against the first: no difference means you are seeing the page, not a moment of it.';
|
|
105
|
+
/**
|
|
106
|
+
* What this tool's unit is wrong for, so a caller with the other job does not
|
|
107
|
+
* reach for it anyway.
|
|
108
|
+
*
|
|
109
|
+
* ── The distinction this exists to draw ──────────────────────────────────
|
|
110
|
+
*
|
|
111
|
+
* `compare_to` answers *"did this page change since a moment ago, on this
|
|
112
|
+
* same tab"* — a before-and-after over time, one lease, one running build.
|
|
113
|
+
* That is a different question from *"how do these two builds differ"*,
|
|
114
|
+
* where the two things being compared are not two moments of one tab but two
|
|
115
|
+
* separate runs, often of separate processes. This surface's unit is one
|
|
116
|
+
* lease holding one tab, which is the right shape for the first question and
|
|
117
|
+
* the wrong one for the second: a two-build comparison wants something that
|
|
118
|
+
* reads the scene or the DOM directly, not pixels from whichever tab happened
|
|
119
|
+
* to be open.
|
|
120
|
+
*
|
|
121
|
+
* ── Why this is worth a sentence rather than leaving it to be inferred ────
|
|
122
|
+
*
|
|
123
|
+
* A caller framing its task as "compare two builds" will reach for whatever
|
|
124
|
+
* on this surface has the word "compare" in it, and `compare_to` is the only
|
|
125
|
+
* candidate. Nothing here refuses that call — a diff still runs and still
|
|
126
|
+
* answers a real question about the two pixels it was given — so the caller
|
|
127
|
+
* gets an answer that looks like the one it asked for while measuring
|
|
128
|
+
* something else. A routing hint at the point of the call is the only thing
|
|
129
|
+
* that can catch this before the wrong tool is already in use; there is no
|
|
130
|
+
* refusal to word better; the call succeeds.
|
|
131
|
+
*/
|
|
132
|
+
const CAPTURE_BUILD_COMPARISON_CAVEAT = 'For comparing two builds — not two moments of the one tab you hold — a tool reading the scene ' +
|
|
133
|
+
'or DOM directly beats this one: this surface is one lease, one tab, pixels.';
|
|
3
134
|
/** Every tool takes the key except the first and the last (§3.1). */
|
|
4
135
|
const LEASE_KEY = {
|
|
5
136
|
name: 'lease_key',
|
|
@@ -107,7 +238,16 @@ export const TOOL_DEFINITIONS = [
|
|
|
107
238
|
name: 'wait_ms',
|
|
108
239
|
type: 'integer',
|
|
109
240
|
required: false,
|
|
110
|
-
description: 'How long
|
|
241
|
+
description: 'How long the navigation may take before it is abandoned, in whole milliseconds. ' +
|
|
242
|
+
'A bound, not a pause: the call returns as soon as the page is there, so a larger ' +
|
|
243
|
+
'value costs nothing on a page that loads quickly and two calls differing only in ' +
|
|
244
|
+
'this argument tell you nothing about how long the page was given to settle. ' +
|
|
245
|
+
'It bounds the load only, and does not wait for work the page starts afterwards, so ' +
|
|
246
|
+
'a canvas or a lazily-loaded region can still be unfinished when this returns; see ' +
|
|
247
|
+
'browser_capture on how to tell. ' +
|
|
248
|
+
'At most the lease lifetime, because a wait outliving the lease would hold the tab ' +
|
|
249
|
+
'past the point it becomes reclaimable; the refusal names the accepted range. ' +
|
|
250
|
+
'Omit it to leave the browser default in force.',
|
|
111
251
|
},
|
|
112
252
|
],
|
|
113
253
|
},
|
|
@@ -175,7 +315,10 @@ export const TOOL_DEFINITIONS = [
|
|
|
175
315
|
operation: 'capture',
|
|
176
316
|
description: 'Take a picture of the page — and, if you name an earlier capture, what changed since it. ' +
|
|
177
317
|
'Returns paths, never the image itself. A selector and a full page cannot both be asked ' +
|
|
178
|
-
'for. Never refused for cost.'
|
|
318
|
+
'for. Never refused for cost. ' +
|
|
319
|
+
CAPTURE_SETTLE_CAVEAT +
|
|
320
|
+
' ' +
|
|
321
|
+
CAPTURE_BUILD_COMPARISON_CAVEAT,
|
|
179
322
|
arguments: [
|
|
180
323
|
LEASE_KEY,
|
|
181
324
|
{
|
|
@@ -196,11 +339,19 @@ export const TOOL_DEFINITIONS = [
|
|
|
196
339
|
required: false,
|
|
197
340
|
description: 'An earlier capture to diff against. The diff rides here rather than being its own tool.',
|
|
198
341
|
},
|
|
342
|
+
{
|
|
343
|
+
name: 'tier',
|
|
344
|
+
type: 'string',
|
|
345
|
+
required: false,
|
|
346
|
+
description: '"detail" or "max" for a higher resolution. Omit for the default — there is no way to ' +
|
|
347
|
+
'ask for the default by name. "max" also requires reason.',
|
|
348
|
+
},
|
|
199
349
|
{
|
|
200
350
|
name: 'reason',
|
|
201
351
|
type: 'string',
|
|
202
352
|
required: false,
|
|
203
|
-
description: 'Free text, recorded, never refused — why this capture needed more
|
|
353
|
+
description: 'Free text, 8-200 characters, recorded, never refused — why this capture needed more ' +
|
|
354
|
+
'than the default tier. Required with tier="max".',
|
|
204
355
|
},
|
|
205
356
|
],
|
|
206
357
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "browser-broker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Leases over tabs in a fixed set of browsers: bounded capacity, a queue, reclamation, and an enforced capture policy.",
|
|
6
6
|
"type": "module",
|
|
@@ -19,12 +19,13 @@
|
|
|
19
19
|
"check:operations": "node scripts/check-operations.mjs",
|
|
20
20
|
"check:argument-refusals": "node scripts/check-argument-refusals.mjs",
|
|
21
21
|
"check:injected-tests": "node scripts/check-injected-tests.mjs",
|
|
22
|
+
"check:argument-reachability": "node scripts/check-argument-reachability.mjs",
|
|
22
23
|
"typecheck": "tsc --noEmit",
|
|
23
24
|
"lint": "eslint .",
|
|
24
25
|
"format": "prettier --write .",
|
|
25
26
|
"format:check": "prettier --check .",
|
|
26
27
|
"test": "node --test \"tests/**/*.test.mjs\" \"tests/**/*.test.ts\"",
|
|
27
|
-
"check": "npm run check:external-refs && npm run check:doc-links && npm run check:arbitration && npm run check:capture-isolation && npm run check:artifact-path && npm run typecheck && npm run lint && npm run format:check && npm test && npm run check:install && npm run check:operations && npm run check:argument-refusals && npm run check:injected-tests",
|
|
28
|
+
"check": "npm run check:external-refs && npm run check:doc-links && npm run check:arbitration && npm run check:capture-isolation && npm run check:artifact-path && npm run typecheck && npm run lint && npm run format:check && npm test && npm run check:install && npm run check:operations && npm run check:argument-refusals && npm run check:argument-reachability && npm run check:injected-tests && npm run check:package",
|
|
28
29
|
"check:arbitration": "node scripts/check-arbitration.mjs",
|
|
29
30
|
"check:artifact-path": "node scripts/check-artifact-path.mjs",
|
|
30
31
|
"build": "tsc --project tsconfig.build.json",
|