dsh-codex-style-computer-use 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,26 +1,35 @@
1
- # dsh-codex-style-computer-use · Codex-style desktop control
1
+ # Codex-style Computer Use
2
2
 
3
- **Give DeepSeek eyes and hands for Windows.**
3
+ **Bring a Codex-style desktop workflow to DeepSeek Harness.**
4
4
 
5
- Ask your agent to work with desktop apps: see the screen, find controls, click and type. Take over with your own mouse or keyboard whenever needed; the agent waits before continuing. Approval prompts remain enabled by default.
5
+ Describe the goal. Your agent observes the screen, locates controls, clicks and types, then checks the result. Work with native Windows apps from the conversation.
6
6
 
7
- Windows desktop computer use for **DeepSeek Harness**: 18 desktop tools plus an Agent activation entry that observe and drive
8
- native Windows applications through prebuilt C# workers included in the package. No PowerShell 7,
9
- SDK or install-time builds are needed.
7
+ English | [中文](README.zh.md) · [Install](#install-and-update-in-dsh-market) · [Changelog](CHANGELOG.md)
10
8
 
11
- English | [中文](README.zh.md)
9
+ ![Codex-style Windows desktop control: observe, act, verify](docs/images/01-codex-style.png)
12
10
 
13
- Windows only (`os: win32`). Requires Node.js ≥ 22 and the .NET Framework 4.x that Windows ships with.
11
+ - **See the target. Take the action.** Combine screenshots, UI Automation and numbered targets with mouse, keyboard, drag, scroll and window tools.
12
+ - **Take over. Let the agent wait. Hand back and continue.** Version 1.3.0 adds a 1.5-second notice before input. Use your computer normally outside actual actuation; after handback, the agent re-observes and resumes the original task.
13
+ - **Fewer round trips. More efficient token use.** Incremental control observations, cropped or downsampled screenshots and batches of known actions reduce repeated context and model calls.
14
14
 
15
- **1.3.0 desktop lifecycle update:** the deep-blue cue now matches actual actuation, with a 16-pixel pointer jitter allowance and a visible 1.5-second purple notice before input. Human takeover returns a required continuation checkpoint. Session/agent ownership and a native cross-process lease allow only one controller. Call `computer_ctrl {action:"release"}` after desktop work and verification; the conversation continues and later desktop work can open a fresh cycle in the same turn. Normal completion releases automatically without an exit question card. Explicit pauses, exits and pending questions remain authoritative.
15
+ ![Human handoff: prepare, take over, continue](docs/images/02-handoff.png)
16
16
 
17
- The indicators use continuous 2-pixel rails with square screen corners and an 8-pixel glow. Normal working states have no floating badges; handoff, pause and preparation use compact top hints. Content interiors remain transparent. The visual revision preserves state logic and animation timing.
17
+ ![Token efficiency: focused observations, batched actions and result readback](docs/images/03-token-efficiency.png)
18
18
 
19
- **1.2.3 automatic fix:** normal installation/update now installs the approval-attribution correction for computer-use calls on affected DSH hosts. No manual host patch is required for those calls. Host approvals and audit remain authoritative; follow the host's restart indication after updating an already loaded plugin. See [1.2.3](docs/RELEASE-1.2.3.md).
19
+ These are **1.3.0 feature illustrations**. Actual token use depends on the task, model and observation strategy. Approvals remain enabled by default; explicit pause, cancellation and exit remain authoritative.
20
20
 
21
- **1.2.1 attention fix:** fixes a missing crypto import that prevented 1.2.0's native `probe` and `raise` requests from reaching the helper. Updating from 1.2.0 is required for this runtime fix; reinstalling 1.2.0 does not fix it. New releases are staged on npm's `next` tag before promotion to `latest`; the market's normal update follows `latest`. See [1.2.1 validation and release status](docs/RELEASE-1.2.1.md).
21
+ **Ready to install.** 18 desktop tools, with native helpers included. No SDK or install-time compilation. Requires Windows, Node.js ≥ 22 and the .NET Framework 4.x included with Windows. An independent community plugin; Codex describes the interaction style.
22
22
 
23
- **1.2.2 GitHub pre-release:** includes a separately deployed DSH host approval-attribution patch and 14 regression cases. Plugin input and approval behavior is retained from 1.2.1. See [release contents and deployment scope](docs/RELEASE-1.2.2.md); GitHub publication does not update npm or the market automatically.
23
+ <details>
24
+ <summary>Current release and compatibility</summary>
25
+
26
+ **1.3.1** is the current release on npm's `latest` channel: `dsh-codex-style-computer-use@1.3.1`. It adds worker-side OCR with per-word coordinates, change/caret-aware waits, UAC and lock-screen awareness with automatic continuation, and IME-safe clipboard text. Follow the host's restart indication when updating a loaded version. See [1.3.1](docs/RELEASE-1.3.1.md) and [GitHub Releases](https://github.com/ltmroberthk915/dsh-computer-use/releases).
27
+
28
+ Version 1.3.1 reads text from the screen by name, notices the Windows secure desktop and resumes on its own, and keeps every colour on the same DPI-derived edge rail with a visible breathing floor. Version 1.3.0 releases desktop control when work ends while the chat continues.
29
+
30
+ Earlier fixes: [1.2.3 approval attribution](docs/RELEASE-1.2.3.md) · [1.2.1 native attention](docs/RELEASE-1.2.1.md) · [standalone host patch](docs/RELEASE-1.2.2.md).
31
+
32
+ </details>
24
33
 
25
34
  ## What it does
26
35
 
@@ -41,9 +50,9 @@ under `$DSH_HOME/data/computer-use/`.
41
50
 
42
51
  ## Install and update in dsh-market
43
52
 
44
- Once the catalog submission is merged and synced, search **computer-use** and select **dsh-codex-style-computer-use**, by **ltmroberthk915** (npm maintainer: **ltmroberthk**). Click **Install**. Later, use **Update** or **Update all** for versions admitted by the host's release policy. The package includes both native helpers; no PowerShell 7 or build permission is needed.
53
+ Search **computer-use** or **Codex-style** in the market. Look for repository **ltmroberthk915/dsh-computer-use** and npm package **dsh-codex-style-computer-use** (maintainer: **ltmroberthk**), then click **Install**. Use **Update** or **Update all** for stable-channel updates. Both native helpers are included; no PowerShell 7 or build permission is needed.
45
54
 
46
- Before catalog sync, use **Settings → Plugins → Add plugin**. The corrected version is **`dsh-codex-style-computer-use@1.2.1`**; the unversioned name still resolves to 1.2.0 until promotion. Do not paste the GitHub repository URL: that selects the source-download path instead of the small prebuilt npm package. The default non-strict pnpm 11.7.0 configuration can install 1.2.1 immediately; an explicitly strict host policy can require waiting as described below. A newly added bundle can load live on the official Desktop host; check that its tools and skill appear. Restart from the tray when replacing an already loaded version, when the client remains stale, or when DSH reports **restart-required**.
55
+ Alternatively, enter **`dsh-codex-style-computer-use`** in **Settings → Plugins → Add plugin** for the current stable channel, or **`dsh-codex-style-computer-use@1.3.0`** for the version illustrated here. Enter the npm package spec directly. Your host may apply a waiting period to fresh versions; follow its indication. A newly added bundle can load live on supported hosts; check that its tools and skill appear. Restart from the tray when replacing a loaded version, when the client remains stale, or when DSH reports **restart-required**.
47
56
 
48
57
  ### Releases less than 24 hours old
49
58
 
package/README.zh.md CHANGED
@@ -1,27 +1,35 @@
1
- # dsh-codex-style-computer-use · Codex 风格桌面操控
1
+ # Codex 风格 Computer Use
2
2
 
3
- **让 DeepSeek 看得见,也点得动。**
3
+ **更接近 Codex 的电脑操控体验,带到 DeepSeek Harness。**
4
4
 
5
- 把 Windows 应用里的重复操作交给 AI:看屏幕、找控件、点鼠标、敲键盘。AI 实际操控前提示 1.5 秒,非操控期间你可以正常使用电脑;操控中接管后,AI 等待交回并接续原任务。默认保留操作审批。
5
+ 你描述目标,AI 看屏幕、找控件、点鼠标、敲键盘,再检查结果。让原生 Windows 应用里的工作,也能在对话中完成。
6
6
 
7
- 给 **DeepSeek Harness** 用的 Windows 桌面操控插件:18 个桌面工具加一个 Agent 激活入口,通过一个 C# worker
8
- 观察并驱动原生 Windows 应用。发布包已包含编译好的原生组件,无需安装 PowerShell 7、SDK 或执行编译命令。
7
+ [English](README.md) | 中文 · [安装](#在-dsh-market-安装和更新) · [版本记录](CHANGELOG.md)
9
8
 
10
- [English](README.md) | 中文
9
+ ![Codex 风格 Windows 桌面操控:观察、操作、验证](docs/images/01-codex-style.png)
11
10
 
12
- 仅支持 Windows(`os: win32`)。需要 Node.js ≥ 22 与 Windows 自带的 .NET Framework 4.x。
11
+ - **看得准,也点得动。** 截图、控件树与编号定位配合使用,支持点击、输入、拖拽、滚动和窗口操作。
12
+ - **你接手,AI 等待,再接着做。** 1.3.0 提供 1.5 秒操作预告;非实际操控期间正常使用电脑,交回后重新观察并接续原任务。
13
+ - **更少往返,更省 Token。** 控件增量观察、区域与降采样截图,加上连续动作批量执行,减少重复观察和模型往返。
13
14
 
14
- **1.3.0 桌面控制周期升级:**实际操控与深蓝色指示同步;鼠标轻微抖动有 16 像素容差;紫色操作前提示持续 1.5 秒;接管后返回必须继续任务的断点;同一时间只有一个 agent/worker 拥有控制周期。桌面工作和验证结束后调用 `computer_ctrl {action:"release"}` 即可释放,聊天继续,同一轮后来仍可重新开始。正常收尾自动释放,不再弹出结束确认选项单。人工暂停、明确退出和待回答的问题保持有效。
15
+ ![人工接管与续接:先提醒、可接管、再继续](docs/images/02-handoff.png)
15
16
 
16
- 灯效统一为贴合屏幕直角的 2 像素光线和最多 8 像素柔光,四角没有圆弧缺口。普通工作状态不显示悬浮文字牌;让出、暂停和预告使用紧凑提示,正文区域保持透明。此次美工调整不改变状态逻辑或闪烁节奏。
17
+ ![Token 效率:聚焦观察、合并操作、回读验证](docs/images/03-token-efficiency.png)
17
18
 
18
- **1.2.3 自动修复:**正常安装或更新插件,即会为受影响宿主中的 computer-use 调用安装审批归因修正,无需手工修改宿主。审批决定与审计仍由宿主处理;更新已加载插件后按宿主提示重启。详见 [1.2.3](docs/RELEASE-1.2.3.md)。
19
+ 上图为 **1.3.0 功能示意**。实际 Token 用量取决于任务、模型和观察方式。默认保留操作审批;明确暂停、取消和退出始终有效。
19
20
 
20
- **1.2.1 提醒修复:**补齐 crypto 导入,修复 1.2.0 原生 `probe` / `raise` 请求还未发送就抛错的问题。这次是运行时代码修复,需要从 1.2.0 更新;重复安装 1.2.0 无效。新版本先发布到 npm 的 `next`,再提升为 `latest`,市场普通更新跟随 `latest`。详见 [1.2.1 验证和发布状态](docs/RELEASE-1.2.1.md)。
21
+ **安装即用。** 18 个桌面工具,原生组件随包提供,无需 SDK 或安装时编译。支持 Windows、Node.js ≥ 22 和系统自带的 .NET Framework 4.x。独立社区插件;Codex 用于说明交互风格。
21
22
 
22
- 宿主在禁用审批弹窗时仍提示“用户拒绝工具”的问题,详见 [DSH 审批归因补丁与回归测试](docs/APPROVAL-ATTRIBUTION.md)。
23
+ <details>
24
+ <summary>当前版本与兼容性</summary>
23
25
 
24
- **1.2.2 GitHub 预发布:**收录独立部署的宿主审批归因补丁和 14 项回归测试。插件输入与审批行为延续 1.2.1,详见 [发布内容及部署范围](docs/RELEASE-1.2.2.md)。GitHub 发布不会自动更新 npm 或市场版本。
26
+ **1.3.1** 是 npm `latest` 频道的当前版本:`dsh-codex-style-computer-use@1.3.1`。新增 worker 侧 OCR(逐词坐标)、可定位变化与识别光标闪烁的等待、UAC/锁屏感知与自动续接、以及绕过 IME 的剪贴板文本通道。更新已加载的旧版本后,按宿主提示重启。详见 [1.3.1](docs/RELEASE-1.3.1.md) 与 [GitHub Releases](https://github.com/ltmroberthk915/dsh-computer-use/releases)。
27
+
28
+ 1.3.1 可直接从屏幕读字定位控件,察觉 Windows 安全桌面并在其结束后自动续接;所有颜色共用同一条按 DPI 换算的边缘光环,呼吸谷值保持可见。1.3.0 起工具用完可主动释放,聊天继续;正常收尾自动退出。
29
+
30
+ 历史兼容修复:[1.2.3 审批归因](docs/RELEASE-1.2.3.md) · [1.2.1 原生提醒](docs/RELEASE-1.2.1.md) · [独立宿主补丁](docs/RELEASE-1.2.2.md)。
31
+
32
+ </details>
25
33
 
26
34
  ## 它能做什么
27
35
 
@@ -39,9 +47,9 @@
39
47
 
40
48
  ## 在 dsh-market 安装和更新
41
49
 
42
- 市场收录同步后,搜索 **computer-use**,选择 **dsh-codex-style-computer-use**,作者 **ltmroberthk915**(npm 维护者 **ltmroberthk**),点击**安装**。以后通过市场的**更新**或**全部更新**安装符合宿主发布策略的版本。两份原生组件都已随包提供,无需 PowerShell 7、SDK 或构建白名单。
50
+ 在市场搜索 **computer-use** 或 **Codex-style**,认准仓库 **ltmroberthk915/dsh-computer-use**、npm 包 **dsh-codex-style-computer-use**(维护者 **ltmroberthk**),点击**安装**。以后通过市场的**更新**或**全部更新**获取正式频道版本。两份原生组件都已随包提供,无需 PowerShell 7、SDK 或构建白名单。
43
51
 
44
- 目录尚未同步时,在 **设置 → 插件 → 添加插件** 输入修复版 **`dsh-codex-style-computer-use@1.2.1`**。正式标签提升前,省略版本号仍会装到 1.2.0。这里填 npm 包名即可;填写 GitHub 仓库网址会转为下载源码,容易受 GitHub 下载线路影响。默认非严格 pnpm 11.7.0 配置可以直接安装 1.2.1;宿主显式启用严格策略时,才可能需要等待冷却。首次新增 bundle 在官方桌面端可热加载;确认工具与 skill 已出现即可使用。替换已经加载的版本、客户端界面未刷新或界面明确提示 **restart-required / 需要重启** 时,再从托盘完整退出 DSH 并重新打开。
52
+ 也可在 **设置 → 插件 → 添加插件** 输入 **`dsh-codex-style-computer-use`**,安装当前正式频道;要体验本页展示的 1.3.0,输入 **`dsh-codex-style-computer-use@1.3.0`**。这里填 npm 包名即可。宿主可能对新版本设置等待期,以界面提示为准。首次新增 bundle 可在支持的宿主中热加载;确认工具与 skill 已出现即可使用。替换已经加载的版本、客户端界面未刷新或界面提示 **restart-required / 需要重启** 时,从托盘完整退出 DSH 并重新打开。
45
53
 
46
54
  ### 发布未满 24 小时
47
55
 
package/icon.svg ADDED
@@ -0,0 +1,29 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
2
+ <defs>
3
+ <linearGradient id="tile" x1="4" y1="0" x2="28" y2="32" gradientUnits="userSpaceOnUse">
4
+ <stop stop-color="#17243F"/>
5
+ <stop offset="1" stop-color="#05080F"/>
6
+ </linearGradient>
7
+ <radialGradient id="halo" cx="11" cy="27" r="20" gradientUnits="userSpaceOnUse">
8
+ <stop stop-color="#8B5CF6" stop-opacity=".2"/>
9
+ <stop offset="1" stop-color="#8B5CF6" stop-opacity="0"/>
10
+ </radialGradient>
11
+ <linearGradient id="rail" x1="5" y1="6" x2="27" y2="24" gradientUnits="userSpaceOnUse">
12
+ <stop stop-color="#5EEAD4"/>
13
+ <stop offset=".5" stop-color="#38BDF8"/>
14
+ <stop offset="1" stop-color="#A78BFA"/>
15
+ </linearGradient>
16
+ <radialGradient id="glow" cx="18.2" cy="19.4" r="7.6" gradientUnits="userSpaceOnUse">
17
+ <stop stop-color="#38BDF8" stop-opacity=".32"/>
18
+ <stop offset="1" stop-color="#38BDF8" stop-opacity="0"/>
19
+ </radialGradient>
20
+ </defs>
21
+ <rect width="32" height="32" rx="7.5" fill="url(#tile)"/>
22
+ <rect width="32" height="32" rx="7.5" fill="url(#halo)"/>
23
+ <rect x="5.6" y="5.9" width="20.8" height="16" rx="3" fill="#FFFFFF" fill-opacity=".06" stroke="url(#rail)" stroke-width="1.9"/>
24
+ <path d="M5.6 10.1 H26.4" stroke="url(#rail)" stroke-width="1.05" stroke-opacity=".5"/>
25
+ <circle cx="8.5" cy="8" r=".9" fill="#5EEAD4"/>
26
+ <circle cx="11.2" cy="8" r=".9" fill="#A78BFA" fill-opacity=".85"/>
27
+ <circle cx="18.2" cy="19.4" r="7.6" fill="url(#glow)"/>
28
+ <path d="M15.6 12.4 L15.6 21.2 L17.8 19 L19.3 22.4 L20.8 21.7 L19.3 18.5 L22.3 18.5 Z" fill="#F8FAFC" stroke="#070B14" stroke-width=".85" stroke-linejoin="round"/>
29
+ </svg>
Binary file
@@ -3,5 +3,5 @@
3
3
  "platform": "win32",
4
4
  "architecture": "x64",
5
5
  "sourceSha256": "6e17308fc1cb0de1ece6f3164866b6de03ecdf74a42a6e78c0a8e6d02912c3bf",
6
- "binarySha256": "c15feb47d7ec975ccab12fea2ee4300ea0a4699069be21d0fb9d1fedebe26158"
6
+ "binarySha256": "f39e79148d0f1c8fa527f55607eb4e7440e82c3ab5b33162bde773528418fe5a"
7
7
  }
@@ -2,6 +2,6 @@
2
2
  "format": 1,
3
3
  "platform": "win32",
4
4
  "architecture": "x64",
5
- "sourceSha256": "885c7935d5520bc6f865da92e3ab63b29c6515a853e2a3e39ceb2221606acf08",
6
- "binarySha256": "c7ae5f0f77912674a8f6f310b95809eef10941cf986f8aa7691aed7a7f08bff5"
5
+ "sourceSha256": "8427a18e1f161959043f638cab821e701714f3d83138911dc9c4fb2cbeb152bf",
6
+ "binarySha256": "0e9212f628ebd73655de1235b3e7bb4ce176c4b60da16c336f2ebae7397084a2"
7
7
  }
@@ -47,7 +47,11 @@ $refs = @(
47
47
  '/r:System.Web.Extensions.dll', '/r:System.Windows.Forms.dll',
48
48
  "/r:$gac\UIAutomationClient\v4.0_4.0.0.0__31bf3856ad364e35\UIAutomationClient.dll",
49
49
  "/r:$gac\UIAutomationTypes\v4.0_4.0.0.0__31bf3856ad364e35\UIAutomationTypes.dll",
50
- "/r:$gac\WindowsBase\v4.0_4.0.0.0__31bf3856ad364e35\WindowsBase.dll"
50
+ "/r:$gac\WindowsBase\v4.0_4.0.0.0__31bf3856ad364e35\WindowsBase.dll",
51
+ # Inbox OCR (2026-11-04): union winmd ships in the package next to this script.
52
+ "/r:$env:windir\Microsoft.NET\Framework64\v4.0.30319\System.Runtime.dll",
53
+ "/r:$gac\System.Runtime.WindowsRuntime\v4.0_4.0.0.0__b77a5c561934e089\System.Runtime.WindowsRuntime.dll",
54
+ "/r:$PSScriptRoot\winmd\Windows.winmd"
51
55
  )
52
56
 
53
57
  New-Item -ItemType Directory -Path $outDir -Force | Out-Null
package/lib/core/index.js CHANGED
@@ -22,6 +22,10 @@ const __dirname = fileURLToPath(new URL('.', import.meta.url))
22
22
 
23
23
  const DEFAULT_TIMEOUT_MS = 15_000
24
24
  const CAPTURE_TIMEOUT_MS = 30_000
25
+ // A UIA tree walk can block the resident's synchronous COM thread. Bound the
26
+ // caller's wait separately from recovery of a worker that never returns.
27
+ const UIA_READS = new Set(['uia', 'uiaFromPoint', 'uiaFocused'])
28
+ const URGENT_CONTROL = new Set(['cancelInput', 'abort', 'panic', 'exit', 'endAsk'])
25
29
  const INPUT_OPS = new Set(['click', 'shiftClick', 'move', 'drag', 'scroll', 'selectRange',
26
30
  'type', 'key', 'uiaAct', 'windowOp', 'activate', 'clipWrite', 'calibrate', 'selftest'])
27
31
  const isInputOp = (op, args = {}) => INPUT_OPS.has(op) || (op === 'focus' && args.refocus === true)
@@ -148,6 +152,7 @@ export class ComputerUse extends EventEmitter {
148
152
  // bare `Error: spawn UNKNOWN` — no path, no syscall, no Windows code — because NOTHING listened
149
153
  // for this child's 'error' event: an unhandled 'error' on an EventEmitter is thrown, so the rich
150
154
  // error object (which names the file Windows refused) was discarded on the way out.
155
+ const child = this.proc
151
156
  this.proc.on('error', (e) => {
152
157
  let file = '?'
153
158
  try { file = `${existsSync(this.workerExe)} exists, ${statSync(this.workerExe).size} bytes` } catch { file = 'stat failed' }
@@ -165,11 +170,11 @@ export class ComputerUse extends EventEmitter {
165
170
  const line = buf.slice(0, nl).trim()
166
171
  buf = buf.slice(nl + 1)
167
172
  if (!line) continue
168
- this.onLine(line)
173
+ if (this.proc === child) this.onLine(line)
169
174
  }
170
175
  })
171
176
  this.proc.stderr.on('data', (d) => this.log(`[worker:stderr] ${d}`))
172
- this.proc.on('exit', this.onWorkerExit)
177
+ this.proc.on('exit', (code) => { if (this.proc === child) this.onWorkerExit(code) })
173
178
  return new Promise((resolve, reject) => {
174
179
  const t = setTimeout(() => reject(new Error('worker ready timeout')), 10_000)
175
180
  // surface the real reason instead of letting the raw emitter error escape
@@ -185,6 +190,7 @@ export class ComputerUse extends EventEmitter {
185
190
  onWorkerExit (code) {
186
191
  this.ready = false
187
192
  this.proc = null
193
+ this.finishUiaRead(false)
188
194
  for (const [id, p] of this.pending) { clearTimeout(p.timer); p.cleanup?.(); p.reject(workerCallError('worker exited', 'worker-exited', p.op, id)) }
189
195
  this.pending.clear()
190
196
  this.emit('dead', code)
@@ -211,6 +217,14 @@ export class ComputerUse extends EventEmitter {
211
217
  this.syncCooperation(msg.cooperation)
212
218
  return
213
219
  }
220
+ // Secure-desktop transitions (UAC prompt / lock screen, human request 2026-11-04): the
221
+ // worker refuses to dispatch while it stands and clears it the moment the normal desktop
222
+ // returns. Mirror the state so callers can show it, and let the plugin notify the human.
223
+ if (msg.event === 'secure') {
224
+ this.secureDesktop = msg.why === 'active'
225
+ this.emit('secure', msg)
226
+ return
227
+ }
214
228
  // Durable brake events are separate from automatic physical-input handoff.
215
229
  // panic — ESC: the human ABORTED. The machine is stopped; wrap up briefly, then wait.
216
230
  // resume — Ctrl+Alt+R: work may continue (a PAUSE ends; it never ends the cycle).
@@ -236,6 +250,7 @@ export class ComputerUse extends EventEmitter {
236
250
  this.emit(msg.event, msg)
237
251
  return
238
252
  }
253
+ if (this.uiaRead?.id === msg.id) this.finishUiaRead(true)
239
254
  const p = this.pending.get(msg.id)
240
255
  if (!p) return
241
256
  this.pending.delete(msg.id)
@@ -246,6 +261,7 @@ export class ComputerUse extends EventEmitter {
246
261
  else {
247
262
  const error = workerCallError(msg.error || 'worker error', msg.code || 'worker-refusal-or-error', p.op, msg.id,
248
263
  msg.outcome === 'not-dispatched' ? 'not-dispatched' : 'unknown')
264
+ if (msg.glowHealth) error.glowHealth = msg.glowHealth
249
265
  p.reject(error.code === 'HUMAN_REOBSERVE' ? humanReobserveError(error) : error)
250
266
  }
251
267
  }
@@ -266,6 +282,14 @@ export class ComputerUse extends EventEmitter {
266
282
  context?.assertOwnership?.()
267
283
  try { await this.ensureWorker() }
268
284
  catch (error) { throw workerCallError(error.message || String(error), 'worker-unavailable', op, null, 'not-dispatched') }
285
+ const child = this.proc
286
+ if (!child) throw workerCallError('worker exited before dispatch', 'worker-exited', op, null, 'not-dispatched')
287
+ const queueDeadline = Date.now() + timeoutMs
288
+ while (this.uiaRead && !URGENT_CONTROL.has(op)) {
289
+ await this.waitForUiaRead(op, input, signal, queueDeadline)
290
+ if (this.proc !== child) throw workerCallError('worker exited before dispatch', 'worker-exited', op, null, 'not-dispatched')
291
+ if (Date.now() >= queueDeadline) throw workerCallError('deadline expired waiting for the UIA read', 'worker-uia-busy', op, null, 'not-dispatched')
292
+ }
269
293
  if (signal?.aborted) throw workerCallError('owning tool cancelled', 'INPUT_CANCELLED', op, null, 'not-dispatched')
270
294
  beforeDispatch?.()
271
295
  context?.assertOwnership?.()
@@ -282,6 +306,7 @@ export class ComputerUse extends EventEmitter {
282
306
  if (input) signed.__inputSeries = this.inputContext.getStore()?.inputSeries || randomUUID()
283
307
  return new Promise((resolve, reject) => {
284
308
  let written = false
309
+ const read = UIA_READS.has(op) && killOnTimeout ? (this.uiaRead = { id, child, timedOut: false }) : null
285
310
  const cleanup = () => signal?.removeEventListener('abort', abort)
286
311
  const abort = () => {
287
312
  if (!this.pending.delete(id)) return
@@ -293,6 +318,21 @@ export class ComputerUse extends EventEmitter {
293
318
  const timer = setTimeout(() => {
294
319
  this.pending.delete(id)
295
320
  cleanup()
321
+ if (read && this.uiaRead === read) {
322
+ read.timedOut = true
323
+ this.emit('uia-read-settled', false)
324
+ // Drop the response by id when it eventually arrives. A later reply for
325
+ // this id cancels only this watchdog, never a newer worker's deadline.
326
+ const grace = Math.min(timeoutMs, DEFAULT_TIMEOUT_MS)
327
+ read.watchdog = setTimeout(() => {
328
+ if (this.proc !== child || this.uiaRead !== read) return
329
+ this.log(`[core] UIA read ${id} (${op}) still blocked after ${grace}ms recovery grace; stopping worker pid=${child.pid}`)
330
+ try { child.kill() } catch { /* already gone */ }
331
+ }, grace)
332
+ reject(workerCallError(`UIA observation timed out: ${op} (${timeoutMs}ms); worker still finishing this read. Narrow the tree or use a point query once it returns.`,
333
+ 'worker-timeout', op, id, 'not-dispatched'))
334
+ return
335
+ }
296
336
  // A CALL THAT RELEASES THE BRAKE MUST NOT BE KILLED FOR BEING SLOW. `proc.kill()` deletes no
297
337
  // STOP file (there is no ProcessExit handler in worker.cs; `abort` is Environment.Exit(0) at
298
338
  // worker.cs:3045), so killing here would end a question with the brake still up — the exact
@@ -303,18 +343,51 @@ export class ComputerUse extends EventEmitter {
303
343
  // UIA/COM provider call blocks its single-threaded loop forever), so kill it and let the NEXT
304
344
  // call respawn fresh instead of queueing behind a corpse; pending calls are rejected via
305
345
  // onWorkerExit.
306
- if (killOnTimeout) { try { this.proc && this.proc.kill() } catch { /* already gone */ } }
346
+ if (killOnTimeout) { try { if (this.proc === child) child.kill() } catch { /* already gone */ } }
307
347
  reject(workerCallError(`worker timeout: ${op} (${timeoutMs}ms)` +
308
348
  (killOnTimeout
309
349
  ? ' — worker killed, next call respawns'
310
350
  : ' — worker left alive so the brake can still be released'),
311
351
  'worker-timeout', op, id))
312
- }, timeoutMs)
352
+ }, Math.max(0, queueDeadline - Date.now()))
313
353
  this.pending.set(id, { resolve, reject, timer, op, input, cleanup, abort })
314
354
  signal?.addEventListener('abort', abort, { once: true })
315
355
  if (signal?.aborted) { abort(); return }
316
356
  written = true
317
- this.proc.stdin.write(JSON.stringify({ id, op, args: signed }) + '\r\n')
357
+ child.stdin.write(JSON.stringify({ id, op, args: signed }) + '\r\n')
358
+ })
359
+ }
360
+
361
+ finishUiaRead (completed) {
362
+ const read = this.uiaRead
363
+ if (!read) return
364
+ this.uiaRead = null
365
+ clearTimeout(read.watchdog)
366
+ this.emit('uia-read-settled', completed && !read.timedOut)
367
+ }
368
+
369
+ // Only the stdin reader's cancellation command is out of band. Preserve
370
+ // emergency/release requests, but never queue ordinary work behind a slow
371
+ // observation after its caller has given up. Do not replay any request.
372
+ waitForUiaRead (op, input, signal, deadline) {
373
+ const busy = () => workerCallError('native UIA observation still busy; this request was not sent',
374
+ 'worker-uia-busy', op, null, 'not-dispatched')
375
+ if (this.uiaRead.timedOut || Date.now() >= deadline) return Promise.reject(busy())
376
+ return new Promise((resolve, reject) => {
377
+ const finish = (error) => {
378
+ clearTimeout(timer)
379
+ this.off('uia-read-settled', settled)
380
+ this.off('input-cancel', cancel)
381
+ signal?.removeEventListener('abort', cancel)
382
+ if (error) reject(error); else resolve()
383
+ }
384
+ const settled = (completed) => finish(completed ? null : busy())
385
+ const cancel = () => finish(workerCallError('cancelled before dispatch', 'INPUT_CANCELLED', op, null, 'not-dispatched'))
386
+ const timer = setTimeout(() => finish(busy()), Math.max(0, deadline - Date.now()))
387
+ this.once('uia-read-settled', settled)
388
+ if (input) this.once('input-cancel', cancel)
389
+ signal?.addEventListener('abort', cancel, { once: true })
390
+ if (signal?.aborted) cancel()
318
391
  })
319
392
  }
320
393
 
@@ -643,15 +716,21 @@ export class ComputerUse extends EventEmitter {
643
716
  async landmarks (opts = {}) {
644
717
  const step = opts.step ?? 16
645
718
  const insetPct = opts.insetPct ?? 10
719
+ // UIA-ONLY MODE (test report 2026-11-04): an animated neighbour (DSH Pet) keeps flipping the
720
+ // sampled pixel signature, so every refresh came back STALE_MARK. uiaOnly pins identity by
721
+ // window handle / process / geometry alone — the sampled signature is read (for the report)
722
+ // but never compared; dispatch-time occlusion checking still guards the actual click.
723
+ const uiaOnly = opts.uiaOnly === true
646
724
  const fs = await this.frameSig({ hwnd: opts.hwnd, step, insetPct })
725
+ if (uiaOnly) fs.sig = null
647
726
  const hwnd = fs.hwnd
648
- if (!hwnd || !fs.sig) throw staleMark('window snapshot unavailable')
727
+ if (!hwnd || (!fs.sig && !uiaOnly)) throw staleMark('window snapshot unavailable')
649
728
  const cached = this.maps.get(String(hwnd))
650
- const key = JSON.stringify([opts.depth ?? 14, opts.maxNodes ?? 1500, opts.maxMarks ?? 80, opts.minSize ?? 8, opts.role, opts.nameContains])
729
+ const key = JSON.stringify([opts.depth ?? 14, opts.maxNodes ?? 1500, opts.maxMarks ?? 80, opts.minSize ?? 8, opts.role, opts.nameContains, uiaOnly])
651
730
  if (cached && sameMarkFrame(cached.frame, fs) && cached.key === key && !opts.refresh) {
652
731
  this.lastMarks = cached.marks
653
732
  this.lastMarkFrame = { ...fs, step, insetPct }
654
- return { hwnd, marks: cached.marks, count: cached.marks.length, reused: true, ageMs: Date.now() - cached.ts, sig: fs.sig }
733
+ return { hwnd, marks: cached.marks, count: cached.marks.length, reused: true, ageMs: Date.now() - cached.ts, sig: fs.sig, uiaOnly }
655
734
  }
656
735
  const uia = await this.call('uia', {
657
736
  hwnd: hwnd || undefined,
@@ -662,6 +741,7 @@ export class ComputerUse extends EventEmitter {
662
741
  })
663
742
  const marks = buildMarks(uia.flat || [], opts.maxMarks ?? 80, opts.minSize ?? 8)
664
743
  const after = await this.frameSig({ hwnd, step, insetPct })
744
+ if (uiaOnly) after.sig = null
665
745
  if (!sameMarkFrame(fs, after)) {
666
746
  const changed = ['hwnd', 'pid'].filter(k => fs[k] !== after[k])
667
747
  for (const k of ['x', 'y', 'width', 'height']) if (fs.region?.[k] !== after.region?.[k]) changed.push(`region.${k}`)
@@ -674,7 +754,7 @@ export class ComputerUse extends EventEmitter {
674
754
  this.lastMarkFrame = { ...after, step, insetPct }
675
755
  this.maps.set(String(hwnd), { marks, frame: after, sig: after.sig, key, ts: Date.now() })
676
756
  if (this.maps.size > 16) this.maps.delete(this.maps.keys().next().value)
677
- return { hwnd, marks, count: marks.length, reused: false, sig: fs ? fs.sig : null, elements: (uia.flat || []).length }
757
+ return { hwnd, marks, count: marks.length, reused: false, sig: fs ? fs.sig : null, uiaOnly, elements: (uia.flat || []).length }
678
758
  }
679
759
 
680
760
  // The human's emergency brake, reachable from the model side too (ESC / Ctrl+Alt+Q are
@@ -748,6 +828,8 @@ export class ComputerUse extends EventEmitter {
748
828
  }
749
829
 
750
830
  async clipRead () { return this.call('clipRead', {}) }
831
+ /** Worker-side OCR: text + per-word boxes in ABSOLUTE screen pixels (2026-11-04). */
832
+ async ocr (region) { return this.call('ocr', region ? { ...region } : {}) }
751
833
  async clipWrite (text) { return this.actuate('clipWrite', { text }) }
752
834
 
753
835
  async activateWindow (sel) { return this.actuate('activate', sel) }
@@ -837,8 +919,13 @@ function staleMark (reason) {
837
919
  }
838
920
 
839
921
  function sameMarkFrame (a, b) {
840
- return !!a?.sig && a.sig === b?.sig && a.hwnd === b.hwnd && a.pid === b.pid &&
841
- ['x', 'y', 'width', 'height'].every(k => Number.isFinite(a.region?.[k]) && a.region[k] === b.region?.[k])
922
+ const identity = a?.hwnd === b?.hwnd && a?.pid === b?.pid &&
923
+ ['x', 'y', 'width', 'height'].every(k => Number.isFinite(a.region?.[k]) && a.region[k] === b.region[k])
924
+ if (!identity) return false
925
+ // A uiaOnly landmark frame (sig === null, test report 2026-11-04) deliberately carries no
926
+ // pixel signature: identity is geometry alone, and the click path's occlusion check remains.
927
+ if (a.sig === null) return true
928
+ return !!a?.sig && a.sig === b?.sig
842
929
  }
843
930
 
844
931
  /** Keep secrets out of the audit log. */
@@ -13,7 +13,12 @@ const framework = env => path.join(env.SystemRoot || env.windir || 'C:\\Windows'
13
13
  export function workerReferences (env = process.env) {
14
14
  const gac = path.join(env.SystemRoot || env.windir || 'C:\\Windows', 'Microsoft.NET', 'assembly', 'GAC_MSIL')
15
15
  return ['System.dll', 'System.Core.dll', 'System.Drawing.dll', 'System.Web.Extensions.dll', 'System.Windows.Forms.dll',
16
- ...['UIAutomationClient', 'UIAutomationTypes', 'WindowsBase'].map(name => path.join(gac, name, 'v4.0_4.0.0.0__31bf3856ad364e35', `${name}.dll`))]
16
+ ...['UIAutomationClient', 'UIAutomationTypes', 'WindowsBase'].map(name => path.join(gac, name, 'v4.0_4.0.0.0__31bf3856ad364e35', `${name}.dll`)),
17
+ // Inbox OCR (2026-11-04): the legacy csc cannot compose the split WinMetadata winmds, so the
18
+ // union Windows.winmd ships inside the package and the WinRT interop pieces come from the OS.
19
+ path.join(framework(env), 'System.Runtime.dll'),
20
+ path.join(gac, 'System.Runtime.WindowsRuntime', 'v4.0_4.0.0.0__b77a5c561934e089', 'System.Runtime.WindowsRuntime.dll'),
21
+ path.join(import.meta.dirname, 'winmd', 'Windows.winmd')]
17
22
  }
18
23
 
19
24
  export function compileNativeBinary (source, output, references, { env = process.env } = {}) {
Binary file