dsh-codex-style-computer-use 2.2.5 → 2.2.7
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 +2 -1
- package/README.zh.md +2 -2
- package/lib/cycle.js +5 -3
- package/lib/index.js +25 -4
- package/lib/leave-timer.js +201 -0
- package/lib/tools.js +1 -1
- package/package.json +1 -1
- package/scripts/check-behavior.mjs +1 -1
- package/skills/computer-use/SKILL.md +15 -15
- package/skills/computer-use/references/lifecycle.md +4 -2
package/README.md
CHANGED
|
@@ -187,7 +187,7 @@ fields are detected through UI Automation's `IsPassword` and refused by default.
|
|
|
187
187
|
## Safety model
|
|
188
188
|
|
|
189
189
|
- **Shared input** — ordinary user activity is allowed while the agent is thinking, observing or preparing. During actual actuation, typing, scrolling and buttons yield immediately; pointer movement has a 16-pixel displacement tolerance. Continued input or a held key/button for 2 seconds enters automatic waiting. After release and 3 quiet seconds, the agent must inspect the current target and continue the original task, without blindly replaying partial writes. A gentle pale-yellow edge gradient shows human ownership.
|
|
190
|
-
- **Release by
|
|
190
|
+
- **Release by desktop block, not by every call** — one cycle covers one continuous desktop block, not the whole task. When you leave that block for publish, upload, install, build, test, network, a file write, analysis or chat, call `computer_ctrl {action:"release"}` first. Keep the cycle only while the next call is still in the same block, including one quick readback you type straight back. An artifact in hand (token, clipboard text, value, path, screenshot) is a signal that the block may be ending, not an automatic release point. Do not hold "in case I need the desktop later"; reacquiring the lease is cheap, while a lit border and an exclusive lease during a long command are not. `hold` is only for position-sensitive readback inside the block, never across a build/publish/upload or long command; `free` then `release`. A deterministic **100-second leave backstop** (`leaveDesktopGraceMs`; 0 disables) releases the lease if no `computer_*` call starts within the grace after a non-desktop tool; it is owner-scoped, cancelled by an actual computer start, never releases a newer cycle, and `hold` does not extend it.
|
|
191
191
|
- **Manual pause** — only physical `Ctrl+Esc` raises a persistent input brake; `Ctrl+Alt+R` resumes. Ordinary Esc, typing, scrolling, pointer distance/frequency and the screen corner do not create this pause. Explicit question/diagnostic holds remain separate.
|
|
192
192
|
- **Exit** — `Ctrl+Alt+Q` ends the session: the overlay goes away, its listeners are torn down, and
|
|
193
193
|
further actuation is refused until a later turn opens a new one.
|
|
@@ -216,6 +216,7 @@ fields are detected through UI Automation's `IsPassword` and refused by default.
|
|
|
216
216
|
| `actionFeedbackDetail` | `compact` | `compact`, `full` or `none`; a call's `feedback` overrides this default; uncached evidence falls back to full inline output |
|
|
217
217
|
| `dryRun` | `false` | simulate actuations, log only |
|
|
218
218
|
| `maxActionsPerMinute` | `60` | core-level actuation rate limit |
|
|
219
|
+
| `leaveDesktopGraceMs` | `100000` | grace after leaving the desktop for another tool; automatic lease expiry, `0` disables |
|
|
219
220
|
| `annotateMarks` | `true` | draw Set-of-Marks boxes on screenshots |
|
|
220
221
|
| `workerExe` | `""` | explicit worker path; empty = autodiscover or compile |
|
|
221
222
|
| `snapshotDir` | `""` | screenshot directory; empty = `$DSH_HOME/data/computer-use/shots` |
|
package/README.zh.md
CHANGED
|
@@ -178,8 +178,7 @@ ID**:派发输入前会核对窗口身份与采样像素,因此过期的 mar
|
|
|
178
178
|
## 安全模型
|
|
179
179
|
|
|
180
180
|
- **人机共用输入** —— 思考、观察和预告期间允许用户正常操作。实际操控时,键盘、滚轮和按钮立即触发让出;鼠标位移有 16 像素容差。持续输入或按住键/按钮 2 秒进入自动等待;全部松开并静默 3 秒后,agent 必须重新观察并接续原任务,不盲目重放部分写入。人操作时显示温和的淡黄色边缘渐变。
|
|
181
|
-
-
|
|
182
|
-
- **不要“等会儿可能还要用”而持有** —— 明确的 `computer_ctrl {action:"hold"}` 是唯一例外;`free` 后立刻 `release`。如果已经调用了非 `computer_*` 工具而 cycle 还开着,说明漏了释放,应立即 release。
|
|
181
|
+
- **按“桌面块”释放,不按每次调用** —— 一个 cycle 覆盖一段连续的桌面工作,不是整个任务。离开这一段去做发布、上传、安装、构建、测试、网络、写文件、分析或聊天时,先 `computer_ctrl {action:"release"}`;只有下一步仍是同一桌面块里的普通点击/输入,或一次很快的读数后马上输入回去,才保留 cycle。拿到的 token/值/路径/截图只是“这块可能快结束了”的信号,不是自动释放点;如果下一步是粘贴/回读/继续点,就保留。不要为了“等会儿可能还要用”而持有;重新获取租约很便宜,长命令期间亮着边框和独占锁并不便宜。`hold` 只用于块内对位置敏感的读数,不能跨 build/publish/upload 或长命令;`free` 后 release。插件另有 **100 秒离开计时兜底**(`leaveDesktopGraceMs`,0 可关):非桌面工具开始后若 100 秒内没有 `computer_*` 调用,插件会 owner-checked 地自动释放;它会在真实 computer 调用开始时取消、不会释放新 cycle,`hold` 也不延长它。暂停和待回答问题不会被自动清除。
|
|
183
182
|
- **一个操作者** —— 会话/agent 所有权与跨 worker 独占锁覆盖整个控制周期,另一 agent 必须等当前操作者释放。
|
|
184
183
|
- **人工暂停** —— 只有实体 `Ctrl+Esc` 触发持续输入刹车,`Ctrl+Alt+R` 恢复。普通 Esc、打字、滚动、鼠标距离/频率和屏幕左上角不再触发这种暂停;显式提问、诊断暂停保持独立。
|
|
185
184
|
- **退出** —— `Ctrl+Alt+Q` 结束会话:覆盖层消失、监听全部拆除,后续注入被拒,直到之后某一轮重新开启。
|
|
@@ -198,6 +197,7 @@ ID**:派发输入前会核对窗口身份与采样像素,因此过期的 mar
|
|
|
198
197
|
| `actionFeedbackDetail` | `compact` | `compact`、`full` 或 `none`;单次调用的 `feedback` 可覆盖默认值;无法缓存时完整内联返回证据 |
|
|
199
198
|
| `dryRun` | `false` | 只模拟操作并记录 |
|
|
200
199
|
| `maxActionsPerMinute` | `60` | 核心层操作限速 |
|
|
200
|
+
| `leaveDesktopGraceMs` | `100000` | 离开桌面去执行非桌面工具后的租约宽限;超时自动释放,`0` 关闭 |
|
|
201
201
|
| `annotateMarks` | `true` | 在截图上绘制 Set-of-Marks 方框 |
|
|
202
202
|
| `workerExe` | `""` | 显式指定 worker 路径;留空则自动发现或编译 |
|
|
203
203
|
| `snapshotDir` | `""` | 截图目录;留空为 `$DSH_HOME/data/computer-use/shots` |
|
package/lib/cycle.js
CHANGED
|
@@ -30,10 +30,12 @@
|
|
|
30
30
|
// only on EVIDENCE that the identity is over (`forgetIdentity`, wired to agent/session disposal),
|
|
31
31
|
// and a forgotten identity is marked DEAD so a late call from it is refused rather than trusted.
|
|
32
32
|
//
|
|
33
|
-
// NO TIME
|
|
34
|
-
//
|
|
33
|
+
// NO IDLE-TIME POLICY LIVES HERE. `openedAt`/`lastAt` are diagnostics only; a paused cycle is never
|
|
34
|
+
// transferred to another session because time passed ("暂停只能由人的恢复/终结解除,时间不是授权").
|
|
35
35
|
// Ownership is released by FACTS: the owner's turn completing, its agent going idle, its identity
|
|
36
|
-
// being disposed, or an exit.
|
|
36
|
+
// being disposed, or an exit. The separate leave timer (lib/leave-timer.js) is not an idle timeout:
|
|
37
|
+
// it is armed only when a non-computer tool starts while the cycle is open, cancelled by any actual
|
|
38
|
+
// computer_* start, retries refusals, and releases only the captured cycle id.
|
|
37
39
|
|
|
38
40
|
/** Session identity without assuming the host's shape (id may be a string, or a field on an object). */
|
|
39
41
|
export function sessionKeyOf (s) {
|
package/lib/index.js
CHANGED
|
@@ -39,6 +39,7 @@ import { scopeOf } from '@deepseek-ai/dsh-scope'
|
|
|
39
39
|
import { createCycleGate, agentKeyOf } from './cycle.js'
|
|
40
40
|
import { recoveryProblem } from './recovery.js'
|
|
41
41
|
import { createCycleLifecycle } from './cycle-lifecycle.js'
|
|
42
|
+
import { createLeaveTimer } from './leave-timer.js'
|
|
42
43
|
import { createTerminalCleanup } from './terminal-cleanup.js'
|
|
43
44
|
import fs from 'node:fs'
|
|
44
45
|
|
|
@@ -402,8 +403,11 @@ export const Config = Schema.object({
|
|
|
402
403
|
workerExe: Schema.string().default('').description('explicit worker.exe path; empty = autodiscover/compile'),
|
|
403
404
|
snapshotDir: Schema.string().default('').description('screenshot dir; empty = $DSH_HOME/data/computer-use/shots'),
|
|
404
405
|
annotateMarks: Schema.boolean().default(true).description('draw SoM boxes on screenshots'),
|
|
405
|
-
// Compatibility with saved settings only.
|
|
406
|
-
calmAfterMs: Schema.number().default(0).description('legacy setting; ignored —
|
|
406
|
+
// Compatibility with saved settings only. Silence/elapsed time never ends a computer-use cycle.
|
|
407
|
+
calmAfterMs: Schema.number().default(0).description('legacy setting; ignored — silence never ends a cycle'),
|
|
408
|
+
// Narrow, explicit exception to the no-idle-timeout rule: armed by LEAVING the desktop for a
|
|
409
|
+
// non-computer tool, cancelled by any computer_* call; 0 disables it. See lib/leave-timer.js.
|
|
410
|
+
leaveDesktopGraceMs: Schema.number().default(100_000).description('release the desktop lease after this long away for another tool; 0 disables'),
|
|
407
411
|
// Compatibility with saved settings; automatic completion has no question or timer.
|
|
408
412
|
turnEndTimeoutAction: Schema.union([
|
|
409
413
|
Schema.const('stop').description('legacy value; ignored'),
|
|
@@ -705,8 +709,16 @@ export function apply (ctx, config) {
|
|
|
705
709
|
paused: pausedOrAsking, inFlight: () => inFlight,
|
|
706
710
|
ownershipOf: exec => exec && typeof exec === 'object' ? callOwners.get(exec) : null,
|
|
707
711
|
})
|
|
712
|
+
const leaveTimer = createLeaveTimer({
|
|
713
|
+
gate, claimOf, log,
|
|
714
|
+
notice: (...args) => notice(...args),
|
|
715
|
+
graceMs: config.leaveDesktopGraceMs,
|
|
716
|
+
setTimer: setTimeout, clearTimer: clearTimeout,
|
|
717
|
+
release: (exec, why, automatic, expected) => lifecycle.release(exec, why, automatic, expected),
|
|
718
|
+
})
|
|
708
719
|
cu.on('handoff', () => {
|
|
709
720
|
lifecycle.settle()?.catch(error => log.warn(`computer-use handback completion failed: ${error.message}`))
|
|
721
|
+
leaveTimer.retryNow()
|
|
710
722
|
})
|
|
711
723
|
|
|
712
724
|
const terminalCleanup = createTerminalCleanup({
|
|
@@ -727,9 +739,10 @@ export function apply (ctx, config) {
|
|
|
727
739
|
}
|
|
728
740
|
gate.assertOwnership(owner.claim, owner.cycleId)
|
|
729
741
|
},
|
|
730
|
-
releaseCycle: (exec, why) => lifecycle.release(exec, why),
|
|
742
|
+
releaseCycle: (exec, why) => { leaveTimer.clear(); return lifecycle.release(exec, why) },
|
|
731
743
|
onStart: (exec) => {
|
|
732
744
|
inFlight++
|
|
745
|
+
leaveTimer.cancel(exec)
|
|
733
746
|
if (isCycleStateOnly(exec?.name, exec?.arguments)) {
|
|
734
747
|
callOwners.set(exec, { claim: claimOf(exec), cycleId: gate.snapshot().id })
|
|
735
748
|
}
|
|
@@ -737,6 +750,7 @@ export function apply (ctx, config) {
|
|
|
737
750
|
onEnd: () => {
|
|
738
751
|
inFlight = Math.max(0, inFlight - 1)
|
|
739
752
|
lifecycle.settle()?.catch(error => log.warn(`computer-use deferred completion failed: ${error.message}`))
|
|
753
|
+
leaveTimer.retryNow()
|
|
740
754
|
},
|
|
741
755
|
// ---- THE WRAP-UP DIRECTIVE (2026-09-15) --------------------------------------------------------
|
|
742
756
|
// WHAT WENT WRONG. A human brake (ESC, a mouse shake, the wheel, typing outside the host) engaged
|
|
@@ -1119,6 +1133,7 @@ export function apply (ctx, config) {
|
|
|
1119
1133
|
const whoKey = agentKeyOf(payload && payload.agent)
|
|
1120
1134
|
if (whoKey && gate.ownsAgent(whoKey)) {
|
|
1121
1135
|
log.info(`owner agent disposed (${whoKey}) — releasing the machine for the next real call`)
|
|
1136
|
+
leaveTimer.onAgentDisposed(payload.agent)
|
|
1122
1137
|
terminalCleanup.request('owner agent disposed')
|
|
1123
1138
|
}
|
|
1124
1139
|
} catch { /* never break the host */ }
|
|
@@ -1169,6 +1184,7 @@ export function apply (ctx, config) {
|
|
|
1169
1184
|
log.info(`ignoring ${kind} turn/end from another session (${describeSession(session)})`)
|
|
1170
1185
|
return
|
|
1171
1186
|
}
|
|
1187
|
+
leaveTimer.onTurnEnd(session)
|
|
1172
1188
|
if (kind === 'aborted' || kind === 'interrupted') {
|
|
1173
1189
|
if (selfAbortUntil && Date.now() < selfAbortUntil) {
|
|
1174
1190
|
selfAbortUntil = 0
|
|
@@ -1195,6 +1211,10 @@ export function apply (ctx, config) {
|
|
|
1195
1211
|
// pure function; its {kind: allow|deny|ask} rides the tools/pre-execute
|
|
1196
1212
|
// waterfall into the harness approval UI (fail-closed, one-shot grants).
|
|
1197
1213
|
ctx.on('tools/pre-execute', (exec, next) => {
|
|
1214
|
+
// This hook is on the approval path. A timer bug must never deny a non-computer tool, so the
|
|
1215
|
+
// leave timer runs in its own fail-open guard; the policy try/catch below stays fail-closed for
|
|
1216
|
+
// computer_* calls only.
|
|
1217
|
+
try { leaveTimer.observe(exec) } catch (error) { log.warn(`computer-use leave timer observe failed: ${error.message}`) }
|
|
1198
1218
|
try {
|
|
1199
1219
|
if (!exec || typeof exec.name !== 'string' || !exec.name.startsWith('computer_')) return next()
|
|
1200
1220
|
if (exec.name === ACTIVATION_TOOL) return next() // capability discovery does not open a control cycle
|
|
@@ -1275,6 +1295,7 @@ export function apply (ctx, config) {
|
|
|
1275
1295
|
}
|
|
1276
1296
|
})
|
|
1277
1297
|
|
|
1298
|
+
ctx.effect(() => () => leaveTimer.dispose())
|
|
1278
1299
|
ctx.effect(() => {
|
|
1279
1300
|
const disposer = () => { return cu.kill().catch(() => {}).finally(detachAudit) }
|
|
1280
1301
|
return disposer
|
|
@@ -1285,5 +1306,5 @@ export function apply (ctx, config) {
|
|
|
1285
1306
|
installToolExposure({ ctx, definitions, defineTool, scopeOf, log, enabled: config.progressiveTools !== false,
|
|
1286
1307
|
skillPath: fileURLToPath(new URL('SKILL.md', SKILL_DIR_URL)),
|
|
1287
1308
|
readSkill: () => fs.readFileSync(fileURLToPath(new URL('SKILL.md', SKILL_DIR_URL)), 'utf8') })
|
|
1288
|
-
log.info(`ready (mode=${config.automationMode}${config.dryRun ? ' dry-run' : ''}; cycle idle timeout disabled)`)
|
|
1309
|
+
log.info(`ready (mode=${config.automationMode}${config.dryRun ? ' dry-run' : ''}; cycle idle timeout disabled${config.leaveDesktopGraceMs ? `; leave timer ${config.leaveDesktopGraceMs} ms` : ''})`)
|
|
1289
1310
|
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// lib/leave-timer.js — the deterministic backstop for a cycle that is left open.
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS EXISTS (2026-11-08). The skill's cycle-boundary rule is a decision rule, not an
|
|
4
|
+
// enforcement mechanism: an agent can copy a desktop artifact and then run a long shell/network
|
|
5
|
+
// command without releasing. The desktop lease then stays open, the border stays lit and every
|
|
6
|
+
// other agent is blocked. The plugin already sees every top-level tool call, so when the agent
|
|
7
|
+
// stays away from the desktop for long enough we release the lease ourselves.
|
|
8
|
+
//
|
|
9
|
+
// WHAT THIS IS NOT. It is not an idle timeout: mere thinking never arms it, and a paused/asking
|
|
10
|
+
// cycle is never released by time. It is armed only when a tool that is not part of the desktop
|
|
11
|
+
// block starts, then cancelled by any later computer_* call. A quick read → compute → type flow
|
|
12
|
+
// therefore never releases, while a six-minute publish releases after the grace. Ownership is
|
|
13
|
+
// captured at arm time and passed to release as the expected cycle, so a late timer can never
|
|
14
|
+
// release a newer cycle. A refused release (pause/handoff/in-flight) is retried instead of being
|
|
15
|
+
// silently consumed.
|
|
16
|
+
|
|
17
|
+
/** Local bookkeeping / cheap inspection helpers that must not arm the leave timer. */
|
|
18
|
+
export const LEAVE_TOOL_EXEMPT = new Set([
|
|
19
|
+
'todo_write',
|
|
20
|
+
'read',
|
|
21
|
+
'write',
|
|
22
|
+
'edit',
|
|
23
|
+
'glob',
|
|
24
|
+
'grep',
|
|
25
|
+
'run_code', // may wrap computer_* calls; the block-boundary skill covers that host shape
|
|
26
|
+
'ask_user_question',
|
|
27
|
+
'terminal_read',
|
|
28
|
+
'terminal_list',
|
|
29
|
+
'terminal_close',
|
|
30
|
+
])
|
|
31
|
+
|
|
32
|
+
/** Desktop observation helpers: using one means the agent is still in the desktop block. */
|
|
33
|
+
const DESKTOP_HELPERS = new Set(['see_now', 'read_image'])
|
|
34
|
+
|
|
35
|
+
export function createLeaveTimer ({
|
|
36
|
+
gate,
|
|
37
|
+
claimOf,
|
|
38
|
+
release,
|
|
39
|
+
graceMs = 100_000,
|
|
40
|
+
retryMs = 10_000,
|
|
41
|
+
setTimer = setTimeout,
|
|
42
|
+
clearTimer = clearTimeout,
|
|
43
|
+
log = { info () {}, warn () {} },
|
|
44
|
+
notice = () => {},
|
|
45
|
+
} = {}) {
|
|
46
|
+
let timer = null
|
|
47
|
+
let retryTimer = null
|
|
48
|
+
let armed = null
|
|
49
|
+
let pending = null
|
|
50
|
+
let disposed = false
|
|
51
|
+
|
|
52
|
+
const unref = t => { try { t?.unref?.() } catch { /* host timer */ } }
|
|
53
|
+
const stop = t => { try { if (t) clearTimer(t) } catch { /* already gone */ } }
|
|
54
|
+
const isLive = () => !disposed && !!gate?.isLive?.()
|
|
55
|
+
const isOpen = () => isLive() && gate?.state?.() === 'open'
|
|
56
|
+
const owned = claim => !!claim && (
|
|
57
|
+
(!!claim.agentKey && !!gate?.ownsAgent?.(claim.agentKey)) ||
|
|
58
|
+
(!!claim.sessionKey && !!gate?.ownsSession?.(claim.sessionKey))
|
|
59
|
+
)
|
|
60
|
+
const dropTimer = () => { stop(timer); timer = null }
|
|
61
|
+
const dropRetry = () => { stop(retryTimer); retryTimer = null }
|
|
62
|
+
|
|
63
|
+
function clear () {
|
|
64
|
+
dropTimer()
|
|
65
|
+
dropRetry()
|
|
66
|
+
armed = null
|
|
67
|
+
pending = null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function cancel (exec) {
|
|
71
|
+
if (!armed && !pending && !timer && !retryTimer) return
|
|
72
|
+
if (exec) {
|
|
73
|
+
let claim
|
|
74
|
+
try { claim = claimOf(exec) } catch { return }
|
|
75
|
+
if (!owned(claim)) return // a foreign/denied computer call must not cancel the owner's timer
|
|
76
|
+
}
|
|
77
|
+
clear()
|
|
78
|
+
log.info?.('computer-use: leave timer cancelled by desktop activity')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function scheduleRetry () {
|
|
82
|
+
if (disposed || retryTimer || !pending || !isLive()) return
|
|
83
|
+
retryTimer = setTimer(() => {
|
|
84
|
+
retryTimer = null
|
|
85
|
+
attempt()
|
|
86
|
+
}, retryMs)
|
|
87
|
+
unref(retryTimer)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function attempt () {
|
|
91
|
+
if (disposed || !pending || !isLive()) return
|
|
92
|
+
const p = pending
|
|
93
|
+
let result
|
|
94
|
+
try {
|
|
95
|
+
result = release(p.exec, p.why, true, p.expected)
|
|
96
|
+
} catch (error) {
|
|
97
|
+
return failed(error)
|
|
98
|
+
}
|
|
99
|
+
if (result && typeof result.then === 'function') result.then(done, failed)
|
|
100
|
+
else done(result)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function done (result) {
|
|
104
|
+
if (disposed || !pending) return
|
|
105
|
+
if (result?.released) {
|
|
106
|
+
const finished = pending
|
|
107
|
+
pending = null
|
|
108
|
+
dropRetry()
|
|
109
|
+
log.info?.('computer-use: leave timer released the desktop lease')
|
|
110
|
+
try {
|
|
111
|
+
const out = notice(
|
|
112
|
+
'computer-use: 桌面租约已自动释放(离开桌面超过 100 秒)。下一次操作会重新观察;旧 mark 已失效。',
|
|
113
|
+
'computer-use: lease expired',
|
|
114
|
+
{ agent: finished?.exec?.agent },
|
|
115
|
+
)
|
|
116
|
+
out?.catch?.(() => {})
|
|
117
|
+
} catch { /* a notice must never break the timer */ }
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
log.info?.(`computer-use: leave timer release refused (${result?.reason || 'no reason'})`)
|
|
121
|
+
scheduleRetry()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function failed (error) {
|
|
125
|
+
if (disposed) return
|
|
126
|
+
if (error?.code === 'COMPUTER_OWNER_CHANGED') {
|
|
127
|
+
// The captured cycle is gone or a newer one exists; this timer must never touch it.
|
|
128
|
+
pending = null
|
|
129
|
+
dropRetry()
|
|
130
|
+
log.info?.('computer-use: leave timer discarded (cycle/owner changed)')
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
log.warn?.(`computer-use: leave timer release failed (${error?.message || error})`)
|
|
134
|
+
scheduleRetry()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function fire () {
|
|
138
|
+
timer = null
|
|
139
|
+
if (disposed || !armed || !isLive()) { armed = null; return }
|
|
140
|
+
pending = armed
|
|
141
|
+
armed = null
|
|
142
|
+
attempt()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function arm (exec) {
|
|
146
|
+
if (disposed || armed || pending || timer || !graceMs || !isOpen()) return
|
|
147
|
+
let claim
|
|
148
|
+
try { claim = claimOf(exec) } catch { return }
|
|
149
|
+
if (!owned(claim)) return // global hook: only the owner's own leaving starts a timer
|
|
150
|
+
const cycle = gate?.snapshot?.()
|
|
151
|
+
if (!cycle || !Number.isInteger(cycle.id)) return
|
|
152
|
+
armed = {
|
|
153
|
+
exec,
|
|
154
|
+
expected: { claim, cycleId: cycle.id },
|
|
155
|
+
why: `left desktop for ${exec.name}`,
|
|
156
|
+
}
|
|
157
|
+
timer = setTimer(fire, graceMs)
|
|
158
|
+
unref(timer)
|
|
159
|
+
log.info?.(`computer-use: leave timer armed for ${graceMs} ms (${exec.name})`)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function observe (exec) {
|
|
163
|
+
if (disposed || !exec || typeof exec.name !== 'string') return
|
|
164
|
+
// A computer_* call — including one waiting in the approval card — means the agent is still on
|
|
165
|
+
// the desktop, so the leave grace must not fire underneath it. A denied call can cancel too;
|
|
166
|
+
// the next non-desktop top-level call arms a fresh grace. onStart() also cancels, idempotently,
|
|
167
|
+
// once the approved tool really begins.
|
|
168
|
+
if (exec.name.startsWith('computer_') || DESKTOP_HELPERS.has(exec.name)) {
|
|
169
|
+
cancel(exec)
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
if (LEAVE_TOOL_EXEMPT.has(exec.name)) return
|
|
173
|
+
arm(exec)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function retryNow () {
|
|
177
|
+
if (!pending || retryTimer || disposed || !isLive()) return
|
|
178
|
+
attempt()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function onTurnEnd (session) {
|
|
182
|
+
if (!isLive()) { clear(); return }
|
|
183
|
+
let claim
|
|
184
|
+
try { claim = claimOf({ session }) } catch { return }
|
|
185
|
+
if (owned(claim)) clear()
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function onAgentDisposed (agent) {
|
|
189
|
+
if (!isLive()) { clear(); return }
|
|
190
|
+
let claim
|
|
191
|
+
try { claim = claimOf({ agent }) } catch { return }
|
|
192
|
+
if (owned(claim)) clear()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function dispose () {
|
|
196
|
+
disposed = true
|
|
197
|
+
clear()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return { observe, cancel, clear, retryNow, onTurnEnd, onAgentDisposed, dispose }
|
|
201
|
+
}
|
package/lib/tools.js
CHANGED
|
@@ -798,7 +798,7 @@ export function registerTools ({ ctx, cu, config, dataDir, log, snapshotPath, de
|
|
|
798
798
|
// ---------------- meta ----------------
|
|
799
799
|
reg({
|
|
800
800
|
name: 'computer_ctrl',
|
|
801
|
-
description: 'hold requests stable page/pointer state between actions (breathing blue); free allows human use while observing (cyan). Neither blocks human takeover. After final verification, release ends desktop control while the conversation continues. stop raises a manual brake, released by human Ctrl+Alt+R; recover checks a temporary diagnostic pause; exit aborts. selftest/calibrate diagnose input; indicator controls the border; acknowledge accepts the skill receipt.',
|
|
801
|
+
description: 'hold requests stable page/pointer state between actions (breathing blue); free allows human use while observing (cyan). Neither blocks human takeover. After final verification, release ends desktop control while the conversation continues; leaving the desktop for another tool can also trigger an automatic lease expiry after the configured grace, so re-observe after it. stop raises a manual brake, released by human Ctrl+Alt+R; recover checks a temporary diagnostic pause; exit aborts. selftest/calibrate diagnose input; indicator controls the border; acknowledge accepts the skill receipt.',
|
|
802
802
|
parameters: {
|
|
803
803
|
action: P('string', 'hold | free | release | clear_cache | selftest | calibrate | stop | exit | indicator | acknowledge | recover', { required: true }),
|
|
804
804
|
why: P('string', 'reason recorded with a stop'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-codex-style-computer-use",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.7",
|
|
4
4
|
"description": "Codex-style Computer Use for DeepSeek Harness: Windows desktop control, screenshots, UI Automation, human input handoff, approval gates and prebuilt native workers. Install and update from dsh-market without PowerShell 7 or build steps.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -37,7 +37,7 @@ const reviewed = new Set([
|
|
|
37
37
|
'cycle-no-timeout', 'cycle-open', 'desktop-acceptance-io', 'desktop-ownership', 'drag-options', 'evidence-protocol',
|
|
38
38
|
'exit-coherence', 'feedback-cache-integration', 'feedback-policy', 'glow-pump', 'glow-visual', 'handoff-continuation',
|
|
39
39
|
'handoff-lock', 'handoff-runtime', 'handoff-watchdog', 'host-gate', 'human-handoff', 'indicator-hold', 'input-stamp', 'key-command',
|
|
40
|
-
'kickoff-focus', 'mcp-contracts', 'mcp-execution', 'modifier-vk', 'monitor-lifetime', 'native-hardening', 'native-respawn',
|
|
40
|
+
'kickoff-focus', 'leave-timer', 'mcp-contracts', 'mcp-execution', 'modifier-vk', 'monitor-lifetime', 'native-hardening', 'native-respawn',
|
|
41
41
|
'native-runtime', 'native-fault-lifecycle', 'observation-archive', 'observations', 'op-classification', 'pointer-lease', 'policy', 'preparation',
|
|
42
42
|
'release-channel', 'renders', 'root-integration', 'session-identity', 'skill-gate', 'steer-message-shape',
|
|
43
43
|
'task-rules', 'terminal-cleanup', 'timeout-branch', 'turnend-agent', 'turnend-rootagent',
|
|
@@ -11,24 +11,24 @@ Read this entry before driving. The skill gate requires this receipt once per se
|
|
|
11
11
|
|
|
12
12
|
Tools load for the current Agent after a successful `skill` load or this receipt; if invisible, call `computer_use_activate` (opens no cycle, releases no brake). Stop/exit and computer_ask work before activation. Issue real tool calls: a target handle is a string argument to computer_uia_act — tags, examples and prose do not execute; in a run_code-only host, call tools through run_code. Observation and the safety direction (ask, stop, exit) are not gated; a human brake still refuses observation. Read a referenced file only when its condition applies.
|
|
13
13
|
|
|
14
|
-
##
|
|
14
|
+
## Cycle boundary (one rule)
|
|
15
15
|
|
|
16
|
-
A desktop cycle
|
|
16
|
+
A desktop cycle covers **one continuous desktop block**, not the user's task and not the turn.
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
When your next call is not `computer_*`, use this two-part test:
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
-
|
|
20
|
+
1. Can you name the next desktop action in this same block?
|
|
21
|
+
2. Is this non-desktop step a short helper needed for that action, rather than a different work item?
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Keep the cycle only if both answers are yes and the helper is quick. Otherwise release first: `computer_ctrl {action:"release"}`. Publish, upload, install, build, test, network, a file write, analysis and a chat answer are different work items, so release before them. `todo_write` or a quick local read between two desktop actions is a helper, not a block boundary. A `run_code` call that itself dispatches the next desktop action, and an observation helper used for that action, remain part of the block.
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
- `computer_shot` returned an image and the next step analyzes or edits it outside the desktop.
|
|
27
|
-
- `computer_uia` / `computer_state` returned a value and the next step is a shell/file/network call.
|
|
28
|
-
- `computer_type` verified a field and the next step is not another `computer_*` call.
|
|
29
|
-
- A save/open dialog returned a path and the next step uses that path in a non-desktop tool.
|
|
25
|
+
An artifact in hand (token, clipboard text, a value, a path, a screenshot) means the desktop block **may** be ending; it is not an automatic release point. If the next call is another desktop action (paste, verify, click), keep the cycle; if it is non-desktop and you cannot name the next desktop action, release.
|
|
30
26
|
|
|
31
|
-
|
|
27
|
+
`hold` is only for a position-sensitive readback inside the continuous block — never across a build, publish, upload or long command. `free`, then release when the readback is done. Pauses, questions and human handoffs still own the desktop; release does not override them.
|
|
28
|
+
|
|
29
|
+
When in doubt, release: reacquiring the lease is cheap, while a lit border and an exclusive lease during long non-desktop work are not.
|
|
30
|
+
|
|
31
|
+
The plugin also has a deterministic backstop: after a non-desktop top-level tool starts, if no `computer_*` call starts within **100 seconds**, it releases the lease automatically (`leaveDesktopGraceMs`; 0 disables). `hold` does not extend this timer. A release is not an error: call `computer_state` again before driving; old marks and cached observations are stale. A `run_code`-only host is covered by this block rule, not by the timer; if you dispatch desktop work through `run_code`, release at the same boundary yourself.
|
|
32
32
|
|
|
33
33
|
## Start, act, verify
|
|
34
34
|
|
|
@@ -38,7 +38,7 @@ Holding “because I may need the desktop again later” is not allowed: reacqui
|
|
|
38
38
|
4. Otherwise use an observed mark or measured coordinate. A mark is a snapshot ID and does not follow scrolling; each mark action checks window identity, position and sampled pixels, and STALE_MARK means no input was dispatched. Refresh with `computer_marks {refresh:true}` and pick by NEW id (old ids retire); refresh after known scrolling/navigation even if a sample misses it. dx/dy offsets work in direct and batch clicks.
|
|
39
39
|
5. Batch a coherent set to a natural checkpoint — no fixed step count. Steps run serially; failure, failed activation or cancellation stops later ones. Reuse returned evidence; `shot:"auto"` adds at most one observation when needed, `shot:"never"` suits text/readback verification. Batch known inputs rather than paying an intermediate image per keystroke.
|
|
40
40
|
6. Verify the outcome: field value, saved record/document, selected range, app status or image. A provider accepting input proves neither a save nor the business result. Report what was observed and any unresolved uncertainty, then finish.
|
|
41
|
-
7. Release at the boundary
|
|
41
|
+
7. Release at the cycle boundary above — when the next step leaves this continuous desktop block for a different work item, or when you cannot name the next desktop action. Do not wait for task completion, a final summary, or the turn end. `computer_ctrl {action:"release"}` closes desktop control only, not the conversation; a later desktop need opens a fresh cycle and requires fresh evidence. If a pause, question or handoff currently owns the desktop, finish that state first. Normal turn completion also releases without a question card; explicit pauses/questions are preserved.
|
|
42
42
|
|
|
43
43
|
`since` (same query + previous observation id) returns added/changed/removed elements and order changes; omitted elements keep their state, while no/expired base, changed scope, partial results or an uneconomical diff return full. `snapshot` retrieves a retained observation without capturing — not fresh verification; page it with `offset`/`limit`. `computer_ctrl {action:"clear_cache"}` clears this Agent's cache without a cycle. Caches are bounded and expire; storage failure falls back to bounded memory and memory pressure to inline results without a fake snapshot id. Omit since/snapshot for fresh evidence after losing the baseline. See [progressive-control.md](references/progressive-control.md).
|
|
44
44
|
|
|
@@ -78,7 +78,7 @@ Before replaying a write, inspect whether it already took effect, then focus, mo
|
|
|
78
78
|
|
|
79
79
|
**Document editors (Word/WPS/PDF) — destructive selection:** never triple-click, never `ctrl+a`, never select-then-paste as an edit strategy. Triple-click selects an unpredictable scope (measured 2026-11-07: it replaced an entire one-page form in Word). Edit one line at a time: click inside the line, `home`, then `shift+end`, CAPTURE and confirm the selection covers only that line, then type. After any unexpected document change press `ctrl+z` immediately and capture before continuing; never save a document you have not seen in full after the edit. Save As a NEW file and never overwrite the original.
|
|
80
80
|
|
|
81
|
-
**Downloads:** resolve the real download directory from `HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders` (`{374DE290-123F-4565-9164-39C4925E467B}`) — a redirected profile puts it outside `%USERPROFILE%`. Edge refuses some HTTP documents with "无法安全下载" and leaves a small `.crdownload` stub; there may be no Keep option. Attempt the download at most twice, then report the block and use an already-downloaded copy or the site's preview if it exists; do not loop on the security UI.
|
|
81
|
+
**Downloads:** resolve the real download directory from `HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders` (`{374DE290-123F-4565-9164-39C4925E467B}`) — a redirected profile puts it outside `%USERPROFILE%`. Edge refuses some HTTP documents with "无法安全下载" and leaves a small `.crdownload` stub; there may be no Keep option. Attempt the download at most twice, then report the block and use an already-downloaded copy or the site's preview if it exists; do not loop on the security UI. Keep the cycle only for this blocked-download retry; elapsed time cannot release a human brake. Use computer_wait for transient readiness; selftest/calibration are for observed input-delivery or coordinate problems, not every failure.
|
|
82
82
|
|
|
83
83
|
For pixel waits pass the observed hwnd to `computer_wait` (stable/change) so animation elsewhere does not delay you; the default covers the whole virtual desktop. A caret inside the region can still prevent settling — verify with UIA state instead of spending full timeouts. `lastDiffPct` describes only the final sample pair; a moved/minimized target needs a fresh observation.
|
|
84
84
|
|
|
@@ -101,7 +101,7 @@ A cold tree may expose only window chrome/an ancestor: use a point query and one
|
|
|
101
101
|
|
|
102
102
|
## Human control and permission
|
|
103
103
|
|
|
104
|
-
Initial observation is free; dispatching input and preserving the scene are the two held states described below. Use `computer_ctrl {action:"hold"}` before a position-sensitive readback and `{action:"free"}` as soon as thinking no longer needs an unchanged desktop; neither creates a cycle nor releases a manual brake. Release the cycle when
|
|
104
|
+
Initial observation is free; dispatching input and preserving the scene are the two held states described below. Use `computer_ctrl {action:"hold"}` before a position-sensitive readback and `{action:"free"}` as soon as thinking no longer needs an unchanged desktop; neither creates a cycle nor releases a manual brake. Release at the cycle boundary above, not when the user's task or the turn ends.
|
|
105
105
|
|
|
106
106
|
During acting/holding a compact tilted blue kite temporarily replaces the pointer; its tip is the exact native hotspot and its centre symbol follows recognized native shapes, neutral when unknown. Native cursor handles and visibility are preserved for cursor checks and screenshot burn-in, so tool images intentionally show the application cursor, not the kite. Free, handoff, pause and release restore native presentation; a short-lived visibility lease restores it if rendering or the worker stops. The symbol proves no target or effect.
|
|
107
107
|
|
|
@@ -17,9 +17,11 @@ The human's own words are the spec here; this section is what they mean.
|
|
|
17
17
|
it ("Computer use 周期结束之后, 所有跟 computer use 相关的监听, 全部都要终结").
|
|
18
18
|
- **A pause NEVER ends the cycle.** The cycle stays live while paused. Normal desktop completion can release an unpaused cycle before the chat turn ends; an explicit Q remains a terminal exit for that turn.
|
|
19
19
|
|
|
20
|
-
## Release
|
|
20
|
+
## Release at the cycle boundary
|
|
21
21
|
|
|
22
|
-
Call `computer_ctrl {action:"release"}`
|
|
22
|
+
Call `computer_ctrl {action:"release"}` when the next step leaves this continuous desktop block for a different work item (publish, upload, install, build, test, network, a file write, analysis or chat), or when you cannot name the next desktop action. This closes the indicator/listeners and releases the exclusive desktop lease while the conversation continues. A quick readback you type straight back, a `run_code`/observation helper that itself continues the desktop action, or a single short helper needed for the next click is still inside the block; keep the cycle. An artifact in hand is a signal that the block may be ending, not an automatic release point. `hold` is only for position-sensitive readback inside the block, never across a long command; `free` then release. When in doubt, release; a later real tool call can open a new cycle in the same turn.
|
|
23
|
+
|
|
24
|
+
The plugin also enforces a deterministic backstop: after a non-desktop tool starts while the cycle is open, a leave timer releases the lease if no `computer_*` call starts within the configured grace (default 100 s; `leaveDesktopGraceMs: 0` disables it). It is owner-scoped, cancelled by an actual `computer_*` start, retries a release refused by a pause/question/handoff/in-flight call, and passes the cycle id captured when it was armed so it cannot release a newer cycle. `hold` does not extend this timer. After an automatic release, re-observe before driving; old marks and cached observations are stale.
|
|
23
25
|
|
|
24
26
|
Release is owner-checked and distinct from `exit`: it does not cancel the conversation or write an EXITED latch. It cannot release an explicit human pause or a pending question. Closure is serialized with ownership acquisition so an old owner's delayed release cannot close a new owner's cycle. An explicit Ctrl+Alt+Q still prevents reopening in that same turn.
|
|
25
27
|
|