dsh-codex-style-computer-use 2.2.6 → 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 -1
- 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 +2 -0
- package/skills/computer-use/references/lifecycle.md +2 -0
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 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`.
|
|
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,7 +178,7 @@ ID**:派发输入前会核对窗口身份与采样像素,因此过期的 mar
|
|
|
178
178
|
## 安全模型
|
|
179
179
|
|
|
180
180
|
- **人机共用输入** —— 思考、观察和预告期间允许用户正常操作。实际操控时,键盘、滚轮和按钮立即触发让出;鼠标位移有 16 像素容差。持续输入或按住键/按钮 2 秒进入自动等待;全部松开并静默 3 秒后,agent 必须重新观察并接续原任务,不盲目重放部分写入。人操作时显示温和的淡黄色边缘渐变。
|
|
181
|
-
- **按“桌面块”释放,不按每次调用** —— 一个 cycle 覆盖一段连续的桌面工作,不是整个任务。离开这一段去做发布、上传、安装、构建、测试、网络、写文件、分析或聊天时,先 `computer_ctrl {action:"release"}`;只有下一步仍是同一桌面块里的普通点击/输入,或一次很快的读数后马上输入回去,才保留 cycle。拿到的 token/值/路径/截图只是“这块可能快结束了”的信号,不是自动释放点;如果下一步是粘贴/回读/继续点,就保留。不要为了“等会儿可能还要用”而持有;重新获取租约很便宜,长命令期间亮着边框和独占锁并不便宜。`hold` 只用于块内对位置敏感的读数,不能跨 build/publish/upload 或长命令;`free` 后 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` 也不延长它。暂停和待回答问题不会被自动清除。
|
|
182
182
|
- **一个操作者** —— 会话/agent 所有权与跨 worker 独占锁覆盖整个控制周期,另一 agent 必须等当前操作者释放。
|
|
183
183
|
- **人工暂停** —— 只有实体 `Ctrl+Esc` 触发持续输入刹车,`Ctrl+Alt+R` 恢复。普通 Esc、打字、滚动、鼠标距离/频率和屏幕左上角不再触发这种暂停;显式提问、诊断暂停保持独立。
|
|
184
184
|
- **退出** —— `Ctrl+Alt+Q` 结束会话:覆盖层消失、监听全部拆除,后续注入被拒,直到之后某一轮重新开启。
|
|
@@ -197,6 +197,7 @@ ID**:派发输入前会核对窗口身份与采样像素,因此过期的 mar
|
|
|
197
197
|
| `actionFeedbackDetail` | `compact` | `compact`、`full` 或 `none`;单次调用的 `feedback` 可覆盖默认值;无法缓存时完整内联返回证据 |
|
|
198
198
|
| `dryRun` | `false` | 只模拟操作并记录 |
|
|
199
199
|
| `maxActionsPerMinute` | `60` | 核心层操作限速 |
|
|
200
|
+
| `leaveDesktopGraceMs` | `100000` | 离开桌面去执行非桌面工具后的租约宽限;超时自动释放,`0` 关闭 |
|
|
200
201
|
| `annotateMarks` | `true` | 在截图上绘制 Set-of-Marks 方框 |
|
|
201
202
|
| `workerExe` | `""` | 显式指定 worker 路径;留空则自动发现或编译 |
|
|
202
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',
|
|
@@ -28,6 +28,8 @@ An artifact in hand (token, clipboard text, a value, a path, a screenshot) means
|
|
|
28
28
|
|
|
29
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
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
|
+
|
|
31
33
|
## Start, act, verify
|
|
32
34
|
|
|
33
35
|
1. Identify the app with `computer_state {windows:true}`, reusing a known window; activate its hwnd with `computer_window` and require `activated:true` before capturing or driving; bind the intended record/field/document too. Minimized: restore, then activate. An occlusion error means inspect what covers it.
|
|
@@ -21,6 +21,8 @@ The human's own words are the spec here; this section is what they mean.
|
|
|
21
21
|
|
|
22
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
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.
|
|
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
|
|
|
26
28
|
Normal owner idle / turn completion releases automatically, without a question card, recommendation timer or a new agent turn. Only a genuine unresolved decision uses `computer_ask`. Keeping the chat alive is not a reason to keep the desktop cycle alive.
|