dsh-inner-browser-use 0.44.0 → 0.46.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/README.md CHANGED
@@ -20,10 +20,13 @@ live picture of that browser, and your own cursor keeps working on the picture.
20
20
  | `browser_key` | Presses one key: Enter, Tab, Escape, Backspace, Delete, Insert, arrows, Home, End, PageUp, PageDown, F1-F12, the numeric keypad, letters, digits, punctuation and every shifted symbol, plus ContextMenu, CapsLock, PrintScreen and media keys. `Ctrl`/`Alt`/`Shift`/`Meta`/`Mod` modifiers, chords with `+`, and `Ctrl+C/X/V/A/Z/Y` run the real editing commands. |
21
21
  | `browser_scroll` | Scrolls the wheel at the centre of the page, or at `x`/`y`. `dy: 600` is about a screen down. |
22
22
  | `browser_shot` | Saves a full-size PNG into `~/.dsh-inner-browser-use/screenshots` and returns the path. |
23
+ | `browser_look` | Looks at a page that draws itself — a game, a canvas, a Flash movie — and returns it as a grid of letters, one per block of screen, the same letter for the same colour: 12×8 costs about 200 characters, so a picture costs no image tokens. A second look reports only the cells that changed. `browser_shot` is still the tool for colour, layout and anything a person should see. |
23
24
  | `browser_media` | Lists the video and sound the page already fetched, with sizes, the one its own player shows first and marked `main`; `save: <row>`, `save: main` or part of the address keeps one. Ads and beacons stay out unless `all: true`; `watch: false` stops remembering the page. A stream is followed to its pieces and joined, so nothing has to be installed. |
24
25
  | `browser_login` | Keeps a sign-in by name: `save` stores this window's cookies and each site's local storage, `list` shows what is kept, `use` signs that account back in and reopens the page, `import` takes a sign-in that is already in Chrome, Brave, Edge or Chromium on this Mac and keeps it under a name, `drop` forgets it. Names survive a restart and belong to the browser rather than to one chat, so two accounts on one site are two names. What sits on disk is encrypted, with the key in the system keychain. |
25
26
  | `browser_state` | Reports whether the browser runs, which profile it uses, its address and title, how many columns are showing it, how much memory it holds right now and when it would close on its own if left alone. |
26
27
 
28
+ A site you read twice is remembered by origin in `~/.dsh/inner-browser-use/sites.json` (override with `DSH_INNER_BROWSER_STATE_DIR`). After two reads, a site whose page text or control count is big opens in `browser_view` as a `map:` of what is there, and a site whose articles are long has `browser_read` cut at 3,500 characters with a line saying so. Both come with one sentence naming the measurement, and both yield to an explicit `mode`, `query` or `max_chars` — learning changes the default, not the answer to a direct question. `scripts/check-sites.mjs` is the live proof: three views of a 300-button page and three reads of a 12,000-character document, saying where the cut fell.
29
+
27
30
  `browser_read` on the same page is the words only, in Markdown (measured: 4,520
28
31
  characters of an about page against 2,987 characters of snapshot — and the
29
32
  snapshot spends most of its budget on controls; on a long article the gap grows
@@ -73,8 +76,16 @@ sign-in here is still there the next day.
73
76
  - **Harness restart.** The same directory is opened again, so cookies, `localStorage` and
74
77
  passwords that Chrome saved all survive.
75
78
  - **Shutdown.** The browser is asked to close rather than killed, which is when Chrome writes
76
- the last of its cookies; an idle browser is closed after 15 minutes, and the clock stops
77
- while a column is watching.
79
+ the last of its cookies.
80
+
81
+ ## When nobody is looking
82
+
83
+ A browser used every two seconds rests after two minutes; one used every ninety seconds waits
84
+ nine. The window is p95 of the person's own last 64 gaps, × 6 for rest and × 2 for a reversible
85
+ pause, so it is learned rather than configured. Close the column and the rest is one minute
86
+ away; leave it open and the page is frozen instead (DOM, scroll and sign-ins kept, screenshot
87
+ stream stopped). A wake is 0.3–1.1 s and lands on the same address. `npm run rest` checks the
88
+ whole chain headlessly; `docs/RESOURCES.md` has the memory ladder and how the neighbours do it.
78
89
 
79
90
  Screenshots are the only thing written outside the profile:
80
91
  `~/.dsh-inner-browser-use/screenshots/`.
package/README.zh.md CHANGED
@@ -19,10 +19,13 @@ profile,不是产品自带的那个浏览页签:右栏放的是它的画面
19
19
  | `browser_key` | 按一个键:Enter、Tab、Escape、Backspace、Delete、上下左右、Home、End、PageUp、PageDown。 |
20
20
  | `browser_scroll` | 在页面中心滚轮,也可以指定 `x`/`y`。`dy: 600` 约一屏。 |
21
21
  | `browser_shot` | 存一张 PNG 到 `~/.dsh/inner-browser-use/screenshots`,返回路径;`region: [x, y, w, h]` 只截一块(游戏、图表、看局部)。 |
22
+ | `browser_look` | 看自己画自己的页面(游戏、canvas、Flash):把画面压成字母网格,一块屏幕一个字母、同色同字母,12×8 约 200 字符,不花图片 token;再看一次只报变了的格子。要颜色、布局或给人看图仍用 `browser_shot`。 |
22
23
  | `browser_media` | 列出页面已经请求过的视频和声音(带大小),页面自己播放器里那个排第一、标 `main`;`save: <行号>`、`save: main` 或地址片段存下来。广告和统计默认不出现(`all: true` 才看),`watch: false` 关掉这个页面的记录。流会顺着播放列表把片段取回拼好,不用装任何东西。 |
23
24
  | `browser_login` | 按名字留住登录:`save` 存下当前窗口的 cookie 和各站的 localStorage,`list` 看有哪些,`use` 切回那个账号并重开页面,`import` 把本机 Chrome/Brave/Edge 里已经登着的站直接留成一个名字,`drop` 忘掉。名字跟着浏览器走、不跟着一个会话走,重启也在;同一站点两个账号就存两个名字。磁盘上那一份是加密的,钥匙在系统钥匙串里。 |
24
25
  | `browser_state` | 报浏览器在不在跑、用的哪个 profile、地址、标题、有几个右栏在看它,以及它此刻占多少内存、没人用的话还有多久关闭。 |
25
26
 
27
+ 同一个站读两次就会被按 origin 记住(`~/.dsh/inner-browser-use/sites.json`,可用 `DSH_INNER_BROWSER_STATE_DIR` 改位置)。第二次之后:正文或控件多的站,`browser_view` 默认先给 `map:` 地标;文章长的站,`browser_read` 默认截到 3500 字符并说明截在哪。两者都带一句实测数字,也都会被显式 `mode`、`query`、`max_chars` 覆盖——学到的是默认值,不是对直接提问的答案。`scripts/check-sites.mjs` 是现场证据:300 个按钮的页看三次、12000 字的文档读三次,报出截断位置。
28
+
26
29
  点、输入、按键之后的那一行会多一句结果:`· navigated to …`(换页了)、`· content changed`(页内有变化)、`· no visible change`(点了没动)、Flash 页则是 `· screen updated` / `· no frame change`;`read: true` 时不再多说。
27
30
 
28
31
  `browser_view` 一次给一页文本:
@@ -65,8 +68,15 @@ profile 是独立目录:`~/.dsh/inner-browser-use/chrome/default`。它在产
65
68
 
66
69
  - **插件重载。** 重载会接上仍在运行的那个 Chrome,页面也还在原处。
67
70
  - **Harness 重启。** 重新打开同一个目录,cookie、`localStorage` 和 Chrome 存下的密码都留着。
68
- - **退出。** 关浏览器是请它自己关(`Browser.close`),Chrome 在这一步才把 cookie 落盘;空闲
69
- 15 分钟会关掉,有右栏在看时不计这段空闲。
71
+ - **退出。** 关浏览器是请它自己关(`Browser.close`),Chrome 在这一步才把 cookie 落盘。
72
+
73
+ ## 没人看的时候
74
+
75
+ 每两秒用一次的人,两分钟后就休息;每九十秒用一次的人能等九分钟——窗口取自这个人最近 64 次
76
+ 调用间隔的 95 分位,休息乘 6、可逆停驻乘 2,是学出来的不是配出来的。右栏关掉后一分钟就真的
77
+ 释放(内存归零,cookies 先存好);右栏还开着只是没人动,就走停驻——DOM、滚动位置、登录态都
78
+ 留着,截图流停掉。唤醒 0.3–1.1 秒,回到同一个地址。无头验证跑 `npm run rest`,内存阶梯与
79
+ 竞品对照在 `docs/RESOURCES.md`。
70
80
 
71
81
  profile 之外只写一样东西:`~/.dsh-inner-browser-use/screenshots/` 里的截图。
72
82
 
@@ -61,9 +61,30 @@ One page per browser. `apply()` only constructs the `Sidecar`; no process exists
61
61
  first tool call, and `browser_state` reports a stopped browser without starting one.
62
62
 
63
63
  Shutdown asks Chrome to close (`Browser.close`), waits briefly, then sends `SIGTERM`, which is
64
- the point where Chrome flushes cookies. An idle timer closes the browser after 15 minutes; a
65
- tool call resets the clock, and a watching column defers the close when the timer fires, so a
66
- page someone is reading outlives the idle window.
64
+ the point where Chrome flushes cookies.
65
+
66
+ Lifetime is a ladder rather than one timer, because a browser that is being watched and one
67
+ that was forgotten deserve different answers (`docs/RESOURCES.md` has the measurements and the
68
+ comparison with other projects):
69
+
70
+ | step | trigger | effect |
71
+ | --- | --- | --- |
72
+ | awake | a tool call, or a mirror watching | none |
73
+ | quiet | no call for `quietAfter()` and a mirror is still open | screencast stopped, page frozen |
74
+ | rest | no call for `restAfter()` and nothing watching | cookies saved, whole tree closed |
75
+
76
+ `rest.js` derives both windows from the gap between the person's own calls: p95 of the last 64
77
+ gaps × 6 for rest, × 2 for quiet, each clamped (`90 s – 15 min`, `20 s – 2 min`). A fresh
78
+ process has no history and uses the floors. `decide()` is pure, so the clock is testable
79
+ without a browser, and the watched count always beats the deadline: a page someone can see is
80
+ frozen at most, never closed.
81
+
82
+ Closing is single-flight (`shutdown()` returns the promise already in flight) and `launch()`
83
+ waits for the outgoing browser before spawning the next one — without that, a rest that fires
84
+ as a call arrives closes the socket the call is about to use. Every 30 s the tree is weighed
85
+ against `DSH_INNER_BROWSER_MEMORY_MB` (default 2400): over budget, idle tabs are reclaimed by
86
+ LRU and casts back off; over again with nobody watching, the browser rests on the spot. Three
87
+ crashes inside ten minutes stop the restart loop and say why instead of trying forever.
67
88
 
68
89
  ## Routes
69
90
 
@@ -134,7 +155,7 @@ browser without pulling the column away from the person reading it.
134
155
 
135
156
  ## Tool surface
136
157
 
137
- `sidecarTools()` in `tools.js` builds the 28 definitions and `index.js` registers them on the
158
+ `sidecarTools()` in `tools.js` builds the 39 definitions and `index.js` registers them on the
138
159
  plugin's tool scope. All of them return one text block. A page read numbers the controls and
139
160
  keeps those numbers across later reads, so `browser_click` by ref means the element the model
140
161
  last saw; navigation drops them and the model gets `REF_GONE` rather than a wrong click.
@@ -198,6 +219,32 @@ characters) and no text, and the tool turns that into a line saying nothing dire
198
219
  question plus those passages. The whole page is still one `browser_view` away, so the expensive
199
220
  fallback — a page pasted into the context for a question it never addressed — is gone.
200
221
 
222
+ ## Looking at a page that draws itself
223
+
224
+ A canvas, a game and a Flash movie have no text tree and no controls: a snapshot reads
225
+ an empty page and a screenshot costs image tokens on every look. `browser_look` takes
226
+ the captured frame, decodes the PNG inside Node (so no page policy and no browser
227
+ decode step is involved) and prints a grid of palette letters — one letter per block of
228
+ screen, the same letter for the same colour. A 12×8 look costs about 200 characters; a
229
+ second look diffs against the first grid and reports only the cells that moved, plus the
230
+ box they moved in. `browser_prepare` is the way in: it reports the surface, the frame
231
+ rate and whether keys arrive, and says when the page needs the Flash runtime.
232
+
233
+ ## Sites that teach the next read
234
+
235
+ Two calls into the same origin, the browser knows what it is looking at. A record per
236
+ origin in `sites.json` holds an exponentially drifted average of the page's character
237
+ count, its control count and its article length, updated by every `browser_view` and
238
+ `browser_read` — measured before the budget cuts the output, so a cut does not shrink
239
+ the site on the next visit. Past two reads, a site whose text or control count is big
240
+ opens `browser_view` as the `map:` view instead of a page of numbered controls, and a
241
+ site whose articles are long has `browser_read` land at 3,500 characters and say where
242
+ it cut. Every learned default carries the measurement that produced it, and every one
243
+ yields to what the model asked for: an explicit `mode`, `query` or `max_chars` is used
244
+ as given. The record lives in `~/.dsh/inner-browser-use/sites.json` (override with
245
+ `DSH_INNER_BROWSER_STATE_DIR`), is written at most once every two seconds, and never
246
+ holds page text — only counts.
247
+
201
248
  ## Answering across tabs
202
249
 
203
250
  A question can be answered by a tab that is not the one on screen. Moving the mirror there and
package/docs/ARTIFACTS.md CHANGED
@@ -1,35 +1,40 @@
1
1
  # 文件落在哪里
2
2
 
3
- 浏览器产生的东西——截图、抓下来的图、打印的 PDF——不落在项目根目录,也不散在 home 下。固定规则:
3
+ 浏览器产生的东西——截图、抓下来的图、打印的 PDF、存下的视频——落在**当前项目里**,跟会话工作目录走;不在项目根目录,也不散在 home 下。
4
4
 
5
5
  ```
6
- ~/.dsh-inner-browser-use/artifacts/<项目>/<会话>/<种类>/
6
+ <项目>/.dsh/browser/<会话>/<种类>/
7
7
  ```
8
8
 
9
- - **项目**:会话工作目录的文件夹名,加路径哈希前 6 位,例如 `dsh-inner-browser-use-a1b2c3`。同名文件夹、不同路径不会混。
10
- - **会话**:会话 id 去掉 `session-` 后的前 8 位,例如 `72557d6e`。同一项目里两个会话各存各的。
9
+ - **项目**:会话的工作目录。换项目就换目录,产出跟着项目走,方便直接拷进仓库或用项目里的工具看。
10
+ - **会话**:会话 id 去掉 `session-` 后的前 8 位,例如 `72557d6e`。同一项目里两个会话各存各的,不会互相覆盖。
11
11
  - **种类**:`shots/` 是 `browser_shot` 的截图,`pictures/` 是 `browser_grab` 抓的图,`pdf/` 是 `browser_pdf` 打印的文档,`media/` 是 `browser_media` 存下的视频、音频和流。
12
12
 
13
- 文件名默认带时间戳(`shot-20261028-124500.png`),给了 `name` 就用它;名字里的分隔符、连续点、开头点都会被去掉,跳不出这个目录。
13
+ `browser_look` 不落文件:它把屏幕读成字母网格直接返回,用来反复看画面变化。
14
+
15
+ 文件名默认带时间戳(`shot-20261028-124500.png`),给了 `name` 就用它;名字里的分隔符、连续点、开头点都会被去掉,跳不出这个目录。`browser_state` 的 `files` 一行给出当前实际落点。
14
16
 
15
17
  每个种类保留最新 40 个文件,旧的自动删,免得跑一天的会话留下一个没人翻得动的文件夹。开着的文件不会被删掉(尽力删除,失败就留着)。
16
18
 
17
- 调用环境里没有项目或会话信息时,落到 `no-project/no-chat/<种类>/`,不猜。
19
+ `<项目>/.dsh/browser/.gitignore` 写一行 `*`,所以截图不会进 `git status`;这个文件只在项目里写。调用环境里没有项目信息时,落到 `~/.dsh-inner-browser-use/artifacts/no-project/`,不猜,也不写 `.gitignore`。
20
+
21
+ `DSH_INNER_BROWSER_ARTIFACTS` 能把整个落点换到一个固定目录(测试用,也方便把产出收到别处),此时不写 `.gitignore`。
18
22
 
19
23
  ## 整个目录
20
24
 
21
25
  ```
26
+ <项目>/.dsh/browser/ 截图、抓图、PDF、媒体(见上)
22
27
  ~/.dsh-inner-browser-use/
23
- artifacts/<项目>/<会话>/<种类>/ 给人看的产出,只在这里(见上)
24
- chrome/ 浏览器自己的数据:登录态、Cookie、缓存
25
- logins/ 按名字留住的登录,一个名字一个文件,加密
26
- vault.key 钥匙串拿不到钥匙时的替代文件,只有本人可读
27
- flash/ Flash 播放器运行时,第一次用 flash:true 才装
28
- browser.log 最近一次浏览器启动和页面记录
29
- asks.log 人问过什么、答了什么
28
+ artifacts/no-project/ 没有项目时的产出
29
+ chrome/ 浏览器自己的数据:登录态、Cookie、缓存
30
+ logins/ 按名字留住的登录,一个名字一个文件,加密
31
+ vault.key 钥匙串拿不到钥匙时的替代文件,只有本人可读
32
+ flash/ Flash 播放器运行时,第一次用 flash:true 才装
33
+ browser.log 最近一次浏览器启动和页面记录
34
+ asks.log 人问过什么、答了什么
30
35
  tabs.json / mirror.json / flash.json / watcher.json 上次开着哪些页
31
36
  ```
32
37
 
33
- `chrome/session-cookies.json` 是 Chrome 退出时会丢的会话 cookie,加密后存在 profile 旁边;`logins/` 与 `vault.key` 由 `browser_login` 和启动时自动保存的登录备份共用,不要手动改或删,要清就用 `browser_login({action:"drop"})`。`chrome/` 其余部分是浏览器进程自己在读写,不要手动改;要清登录态,用 `browser_open` 的档位或插件自己的入口,别删目录里的某个文件。上面这些都在一个根目录下,卸载插件前想留产出就拷 `artifacts/`。
38
+ `chrome/session-cookies.json` 是 Chrome 退出时会丢的会话 cookie,加密后存在 profile 旁边;`logins/` 与 `vault.key` 由 `browser_login` 和启动时自动保存的登录备份共用,不要手动改或删,要清就用 `browser_login({action:"drop"})`。`chrome/` 其余部分是浏览器进程自己在读写,不要手动改;要清登录态,用 `browser_open` 的档位或插件自己的入口,别删目录里的某个文件。
34
39
 
35
40
  测试里用 `DSH_INNER_BROWSER_ARTIFACTS` 指向临时目录,不写真实 home。
package/docs/BENCH.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## 读过的站第二次更便宜(0.45.0,本地页面,`scripts/check-sites.mjs`)
2
+
3
+ 同一台机器、无头 Chrome、两个本地 origin。第一次走老路(整页读),第二次之后用学到的默认值。
4
+ 所有数字来自一次真实运行,不是估算。
5
+
6
+ | 页面 | 第一次 | 学到之后 | 省下 | 说明 |
7
+ | --- | --- | --- | --- | --- |
8
+ | 300 个按钮的页面,`browser_view` | 1801 字符 | 760 字符(`map: 90 controls`) | 58% | 两次之后自动改成地标视图,并说明「this site reads big」 |
9
+ | 12000 字文档,`browser_read` | 6096 字符(满预算) | 3726 字符(截到 3500) | 39% | 截断处自带 `cut at 3500 characters`,用 `max_chars` 可要全文 |
10
+
11
+ - 学的尺子量的是页面本身,不是这次输出:`read.js` 的 `chars` 在截断前取值,所以第一次就把整篇长度记下来,不会被 3500 的预算越读越小
12
+ - 显式 `mode` / `query` / `max_chars` 一律优先;学到的是默认值,附一句带数字的理由
13
+ - 记录写在 `sites.json`,每进程最多每 2 秒一次;两次运行之间只留计数,不留正文
14
+ - `scripts/check-look.mjs`:12×8 的字母网格,三次看共 639 字符,全程没有图片 token
15
+ - `scripts/soak.mjs`:无头浏览器 24 轮开页/读页/截图/重读,34.9 秒,进程树内存 1440 → 1287 MB(−153 MB),页数始终 1,8 次截图、24 次读;没有页签或内存随轮数上涨
16
+
1
17
  ## 读页先读正文(0.44.0,本地页面,`scripts/bench.mjs`)
2
18
 
3
19
  同一台机器、同一档预算。旧法是按页面顺序把预算灌满的整页读。
@@ -1014,3 +1030,23 @@ not.
1014
1030
  | frame size, JPEG q72 | 96–103 KB |
1015
1031
  | mirror SSE | 15 fps(66 ms 一轮),4 s 收 60 帧,均值 97.8 KB |
1016
1032
  | switch cost | 一次浏览器重启(`browser_open({flash:true})` → `browser_open({flash:false})`) |
1033
+
1034
+ ## Phase 24 — 长期运行的资源回收,0.46.0
1035
+
1036
+ 一台窗口隐藏的 Chrome 空转一晚就是几百 MB。这一版把「多久收」交给用法自己决定:记住这个人
1037
+ 最近 64 次调用之间的间隔,取 95 分位,休息乘 6、可逆停驻乘 2,各自夹在上下限里;有人看时
1038
+ 永不关闭、只冻结。关闭与启动串行,避免休息和下一次调用撞在一起。
1039
+
1040
+ | what | measured |
1041
+ | --- | --- |
1042
+ | 20 次调用、间隔 20 秒 | p95 20 s → 休息 120 s,停驻 40 s |
1043
+ | 30 次调用、间隔 90 秒 | p95 90 s → 休息 540 s,停驻 180 s |
1044
+ | 没有历史的新进程 | 用下限:休息 90 s,停驻 20 s |
1045
+ | 右栏关掉(`/sidecar/forget`) | 60 s 后休息,内存归零 |
1046
+ | 唤醒 | 326–1010 ms,回到同一页同一地址 |
1047
+ | soak 24 轮+3 次真实休息 | 40.1 s,内存 1427 → 956 MB,页数始终 1 |
1048
+ | soak 16 轮+2 次真实休息 | 26.8 s,内存 1308 → 900 MB |
1049
+ | 休息与调用相撞 | 关闭单飞、启动等待;0.45.0 会在第 8 轮把新连接关掉(已复现、已修) |
1050
+ | 内存预算 | 2400 MB / 30 s 一次称重;超预算先收 LRU 页签、降码率,无人看才休息 |
1051
+ | 崩溃循环 | 10 分钟内 3 次 → 拒绝启动并说明原因 |
1052
+ | suite | 42 pass(sidecar)· rest 9 pass · soak/rest 端到端全过 |
package/docs/FLASH.md CHANGED
@@ -56,6 +56,16 @@ Electron 11 的两个真实限制,代码里已按它们写:
56
56
  - 它不会用当前 Chrome 的 profile 目录(会降级并上锁),Flash 分支固定用 `<profile>-flash` 独立目录;两边登录态各自独立。
57
57
  - `/json/new` 在 Chromium 87 上是 404,单窗口应用也没有第二个窗口,所以 Flash 分支不建新页签:会话要页签时把唯一那个窗口导航过去。
58
58
 
59
+ ### 起来之后还要证明它真在
60
+
61
+ 端口能连上不等于插件装上了:`/json/version` 由浏览器进程回答,插件加载失败时它照样 200,页面只是画出一块黑。所以启动流程在端口就绪之后还问页面一句 `navigator.plugins`,只认名字里带 `Shockwave Flash` 的那条,结果存成浏览器自己的状态:
62
+
63
+ - `browser_open` 在正文里给一行 `flash runtime: plugin loaded (Shockwave Flash)`,或者 `open, but no Flash plugin is loaded — movies will not play; run scripts/flash-setup.mjs`;还没问到时写 `plugin not checked yet`,不猜。
64
+ - `browser_state` 的运行时行把「模式」和「插件」分开:`Flash-era runtime — plugin loaded (…)` / `— no Flash plugin is loaded`。
65
+ - `browser_prepare` 在真的找到 movie、但插件缺失时补一句「它们跑不起来」,免得把黑帧当成游戏还在加载。
66
+
67
+ 问不到(页面崩溃、CDP 早断)时记 `null`——没有证据就不下结论,这跟「没装运行时」是两回事:后者在启动阶段就抛 `sidecar: no Flash runtime on this machine`。
68
+
59
69
  实测数字(Apple 芯片 + Rosetta,1440×900):
60
70
 
61
71
  | 指标 | 值 |
@@ -0,0 +1,83 @@
1
+ # Long-term resources: what keeps a browser cheap when nobody is looking
2
+
3
+ A sidecar is only worth keeping if leaving it open overnight costs nothing. This
4
+ document describes the ladder a browser steps down when it goes unused, how the
5
+ timings are chosen, and how the same problem is solved by the four projects a
6
+ person is most likely to compare this plugin to.
7
+
8
+ ## The ladder
9
+
10
+ | step | when | what happens | cost while parked |
11
+ | --- | --- | --- | --- |
12
+ | `awake` | calls are arriving, or someone is watching the mirror | nothing | one headless Chrome |
13
+ | `quiet` | no call for a while, but a pane still shows the page | screencast stopped, page frozen (`Page.setWebLifecycleState`) | JS/timers paused, DOM and cookies kept, still one process tree |
14
+ | `rest` | no call for a while and no pane watching | cookies saved, `Browser.close`, whole tree gone | zero |
15
+
16
+ The rest step is what the pane closing at `x` earns: the plugin tells the
17
+ sidecar the mirror is gone, and if nobody else is watching it arms a one-minute
18
+ rest. A chat that never closed its pane still gets the quiet step, which is
19
+ reversible and keeps the page exactly where the person left it.
20
+
21
+ ## Choosing the timings without guessing
22
+
23
+ Fixed timeouts are wrong in both directions: too short and every pause pays a
24
+ cold start, too long and an abandoned browser outlives the work. The rest and
25
+ quiet windows are therefore derived from what the user actually did in this
26
+ process. `rest.js` keeps the last 64 gaps between calls, takes the 95th
27
+ percentile, and multiplies it:
28
+
29
+ ```text
30
+ rest = clamp(p95(gaps) × 6, 90 s, 15 min)
31
+ quiet = clamp(p95(gaps) × 2, 20 s, 2 min)
32
+ ```
33
+
34
+ A person clicking every two seconds gets a two-minute rest; one coming back
35
+ every ninety seconds gets nine minutes. Gaps longer than the maximum rest are
36
+ treated as a new session, not as evidence. With no history at all the floors
37
+ apply, so a fresh process parks quickly instead of waiting for a pattern that
38
+ does not exist yet.
39
+
40
+ The decision itself is a pure function of four numbers — now, last use, the
41
+ rest deadline, and how many mirrors are watching — which is what makes it
42
+ testable without a browser (`test/rest.test.mjs`). The watched count wins over
43
+ the deadline: a pane on screen always means `quiet` at most, never `rest`.
44
+
45
+ ## Memory pressure
46
+
47
+ Timings are the first line; memory is the second. Every 30 s the sidecar weighs
48
+ the whole process tree and compares it with a budget (2400 MB by default,
49
+ `DSH_INNER_BROWSER_MEMORY_MB`). Over budget while a pane is open, it stops
50
+ screencasts and reclaims unused tabs (LRU, pinned pages excluded). Over budget
51
+ again with nobody watching, it puts the browser away on the spot rather than
52
+ waiting for the clock. Being over once is not enough to close a browser the
53
+ person is looking at.
54
+
55
+ ## What breaks a long run, and what is done about it
56
+
57
+ | failure | response |
58
+ | --- | --- |
59
+ | a rest and the next call arrive together | `shutdown()` is single-flight and `launch()` awaits the outgoing browser, so a fresh socket is never closed by the one leaving |
60
+ | the browser dies three times in ten minutes | the crash-loop guard refuses to start it again and says why, instead of spinning |
61
+ | a tab is left in a chat that stopped using it | 10-minute LRU, skipped while it streams or holds a popup |
62
+ | the last call was minutes ago but the pane is open | freeze, not close: scroll position, DOM and sign-ins are still there on wake |
63
+
64
+ ## Neighbours
65
+
66
+ Compared with the projects this one is usually put next to (`--idle-timeout`,
67
+ `keep_alive`, and their documentation):
68
+
69
+ | project | idle handling | what a wake costs | notes |
70
+ | --- | --- | --- | --- |
71
+ | Playwright MCP | one flag, `--idle-timeout`, default one hour for headless, whole browser closed | a cold start | no viewer awareness; each client gets its own Chrome; `src/` is no longer in the repository |
72
+ | Chrome DevTools MCP | none — the browser lives until the process exits | n/a | one dispose chain, no idle clock, screencast never pauses for a detached viewer |
73
+ | browser-use | `keep_alive` flag, no timeout value | n/a | teardown skipped while `keep_alive`; no idle timer, no memory policy |
74
+ | Stagehand | none | n/a | deliberately disables Chrome's background throttling, then tears down and removes the profile |
75
+ | this plugin | viewer-aware ladder, timings learned from use, memory pressure, single-flight close | 0.3–1.1 s, same page and address | freeze keeps the DOM; rest keeps the cookies; nothing cold-starts unless the process is new |
76
+
77
+ The gaps that remain in the field, and that this document treats as the design
78
+ brief: nobody freezes a page instead of closing it; nobody pauses a screencast
79
+ because the viewer looked away; cold restart is the documented recovery
80
+ everywhere; tab caps and LRU are left to the caller; memory is a diagnostic
81
+ rather than a policy; multi-agent isolation is all-or-nothing. Those are the
82
+ five places where this plugin is deliberately ahead rather than equal — see
83
+ `docs/ARCHITECTURE.md` and `docs/BENCH.md` for the measured side of it.
package/docs/VERIFY.md CHANGED
@@ -97,6 +97,67 @@ still hold the cookies.
97
97
  内置浏览器 means the page half is stale: reload the GUI page.
98
98
  - Guest-era logins are not migrated; sign in once in the new profile.
99
99
 
100
+ ## 7. A page that draws itself
101
+
102
+ ```bash
103
+ node scripts/check-look.mjs
104
+ ```
105
+
106
+ Headless Chrome paints a page, `browser_look` returns a 12×8 letter grid, the page
107
+ paints a red square, and the second look names only the cells that moved. Expected:
108
+ `three looks cost 639 characters and no picture`. A look that returns words instead
109
+ of a grid, or a diff of the whole screen, means the PNG was not decoded locally.
110
+
111
+ ## 8. The next visit is cheaper
112
+
113
+ ```bash
114
+ node scripts/check-sites.mjs
115
+ ```
116
+
117
+ Two local origins: a page of 300 buttons and a 12,000-character document. Expected,
118
+ in order: the first view is the page, the second is still the page (one read is not a
119
+ lesson), the third opens as `map: 90 controls` and says `this site reads big (2 read(s),
120
+ ~2946 chars of page, ~90 controls)`; the first document read is the whole thing (6,096
121
+ characters), a forced second read is the whole thing again, and the third comes back
122
+ `cut at 3500 characters` with `this site reads long (2 read(s), ~6000 chars of text)`.
123
+ The run ends with `sites: ok` and leaves `sites.json` in a temporary directory.
124
+
125
+ A third view that is still the page, or a third read that is not cut, means the record
126
+ is written after the default is chosen, or the size recorded is the output's rather
127
+ than the page's: a cut must not shrink the site for the next visit.
128
+
129
+ ## 9. It does not grow
130
+
131
+ ```bash
132
+ npm run soak
133
+ ```
134
+
135
+ Twenty-four cycles of open, read, snapshot, re-read through one headless browser,
136
+ and every eighth cycle the browser is really put away and woken again.
137
+ Expected: `soak: ok · 24 cycles` with `pages 1`, memory lower at the end than at
138
+ the start, and a summary naming at least one rest (`3 rests` here, 1427 → 956 MB
139
+ in 40.1 s). Memory climbing with the cycle count, a second page appearing, or a
140
+ rest that leaves memory held means something is kept per read rather than per
141
+ page, or the rest does not close the whole tree. The run is bounded on purpose —
142
+ under a minute, no internet.
143
+
144
+ ## 10. It comes back where it was
145
+
146
+ ```bash
147
+ npm run rest
148
+ ```
149
+
150
+ A headless sidecar is told to rest 1.2 s after the last call. Expected chain:
151
+ `browser_state` says `rests in`, then `stopped` with `memory 0 MB` once the rest
152
+ lands; touching the browser again wakes it on its own, `pageFor(session).url` is
153
+ still the page that was open, the text is either the same paragraph or
154
+ `nothing changed since the last read`, and the wake takes under 4 s. `rest and
155
+ wake hold.` is the bar.
156
+
157
+ A rest that never happens while nobody is watching means the watched count is
158
+ not reaching the clock; a wake that lands on a blank tab means the page-per-chat
159
+ map was dropped by the close instead of being restored by the launch.
160
+
100
161
  ## Live
101
162
 
102
163
  `node scripts/live.mjs` does the same work on the open internet, with no local
package/index.js CHANGED
@@ -947,7 +947,7 @@ export function apply(ctx) {
947
947
  '页面很长、只想确认某句话/按钮在哪时用 `browser_find`,别整页读。要读长文(文章、文档、issue、论坛帖)用 `browser_read`:它回 Markdown,只花几十分之一的 token,链接可点;要动手点、填表才用 `browser_view` 拿编号。',
948
948
  '要保存页面样子用 `browser_shot`,它会给出 PNG 绝对路径;要交给人看的文件,把绝对路径写进回复。',
949
949
  '页面放视频或音乐、人想留下它时用 `browser_media`:它列的是页面自己已经请求过的媒体(带大小),页面自己播放器里那个标 `main` 排第一,广告和统计请求默认不出现(`all:true` 才看),`save: main` 存第一个;视频是流的话会顺着播放列表把片段取回拼成整段,不用装 yt-dlp 或 ffmpeg。先让页面播一下再列,播放请求才是文件。不想要这个页面的记录就 `watch:false`。',
950
- '游戏、canvas、Flash:画面自己画的地方,`browser_view` 和 `browser_read` 读不出里面是什么。先 `browser_prepare` 拿画面位置、帧率、按键是否到达和下一步;之后看 `browser_shot` 的图定位置,按坐标点、按住、按组合键,再拍一张看画面变了没有——变了就是这一步生效了。当年用 Flash 做的游戏用 `browser_open` 带 `flash:true` 在栏内跑,不装播放器、不开窗口;Flash 默认关,`flash:true` 开、`flash:false` 回到普通 Chrome,两边都只在本会话右栏。',
950
+ '游戏、canvas、Flash:画面自己画的地方,`browser_view` 和 `browser_read` 读不出里面是什么。先 `browser_prepare` 拿画面位置、帧率、按键是否到达和下一步;之后用 `browser_look` 读屏:它把画面压成字母网格、点完再看一次只报变了哪块,不花图片 token;要定位置或看颜色再 `browser_shot`。按坐标点、按住、按组合键后先 `browser_look` 确认这一步生效,别每步截图。当年用 Flash 做的游戏用 `browser_open` 带 `flash:true` 在栏内跑,不装播放器、不开窗口;Flash 默认关,`flash:true` 开、`flash:false` 回到普通 Chrome,两边都只在本会话右栏。',
951
951
  ].join('\n'),
952
952
  }), 'dsh-inner-browser-use: system prompt section')
953
953
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-inner-browser-use",
3
- "version": "0.44.0",
3
+ "version": "0.46.0",
4
4
  "description": "An agent-owned Chromium sidecar mirrored into the DeepSeek Harness right column: the agent drives it, the person watches and can take the mouse, and logins survive restarts.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -14,10 +14,12 @@
14
14
  "files": [
15
15
  "index.js",
16
16
  "sidecar.js",
17
+ "rest.js",
17
18
  "tools.js",
18
19
  "read.js",
19
20
  "review.js",
20
21
  "media.js",
22
+ "pixels.js",
21
23
  "vault.js",
22
24
  "chromecookies.js",
23
25
  "client.js",
@@ -29,6 +31,10 @@
29
31
  "scripts/check-flash-mirror.mjs",
30
32
  "scripts/check-flash-local.mjs",
31
33
  "scripts/check-flash-tools.mjs",
34
+ "scripts/check-look.mjs",
35
+ "scripts/check-sites.mjs",
36
+ "scripts/soak.mjs",
37
+ "scripts/check-rest.mjs",
32
38
  "runtime/",
33
39
  "docs/",
34
40
  "README.md",
@@ -51,7 +57,7 @@
51
57
  "dsh": ">=0.2.0"
52
58
  },
53
59
  "scripts": {
54
- "test": "node --test test/artifacts.test.mjs test/client.test.mjs test/cookies.test.mjs test/diff.test.mjs test/fresh.test.mjs test/host.test.mjs test/input.test.mjs test/isolation.test.mjs test/keyboard.test.mjs test/leak.test.mjs test/login.test.mjs test/media.test.mjs test/no-duplicate.test.mjs test/package.test.mjs test/blocks.test.mjs test/readfault.test.mjs test/refs.test.mjs test/review.test.mjs test/session.test.mjs test/settle.test.mjs test/sidecar.test.mjs test/swf.test.mjs test/tabs.test.mjs test/trouble.test.mjs test/vault.test.mjs",
60
+ "test": "node --test test/artifacts.test.mjs test/client.test.mjs test/cookies.test.mjs test/diff.test.mjs test/fresh.test.mjs test/host.test.mjs test/input.test.mjs test/isolation.test.mjs test/keyboard.test.mjs test/leak.test.mjs test/login.test.mjs test/media.test.mjs test/no-duplicate.test.mjs test/package.test.mjs test/blocks.test.mjs test/rest.test.mjs test/readfault.test.mjs test/refs.test.mjs test/review.test.mjs test/session.test.mjs test/settle.test.mjs test/sidecar.test.mjs test/swf.test.mjs test/tabs.test.mjs test/trouble.test.mjs test/vault.test.mjs",
55
61
  "doctor": "node scripts/doctor.mjs",
56
62
  "breadth": "node scripts/breadth.mjs",
57
63
  "live": "node scripts/live.mjs",
@@ -65,7 +71,11 @@
65
71
  "flash-mirror": "node scripts/check-flash-mirror.mjs",
66
72
  "flash-tools": "node scripts/check-flash-tools.mjs",
67
73
  "flash-local": "node scripts/check-flash-local.mjs",
68
- "keys": "node scripts/keyboard.mjs"
74
+ "keys": "node scripts/keyboard.mjs",
75
+ "look": "node scripts/check-look.mjs",
76
+ "sites": "node scripts/check-sites.mjs",
77
+ "soak": "node scripts/soak.mjs",
78
+ "rest": "node scripts/check-rest.mjs"
69
79
  },
70
80
  "license": "MIT",
71
81
  "author": {